runtime data

This commit is contained in:
lachtan
2026-07-22 12:32:23 +02:00
parent 8e66d6b92a
commit 94dccb3758
11 changed files with 1475 additions and 194 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ tasks/
.nanobot/ .nanobot/
*.bak *.bak
notes/.compile.lock notes/.compile.lock
cron/runs/

View File

@@ -49,6 +49,9 @@ The exec safety guard blocks commands without an explicit workspace path (e.g. `
- Longer / non-trivial script -> Python. - Longer / non-trivial script -> Python.
- Override: if the user explicitly specifies a language or location, their instruction always takes precedence. - Override: if the user explicitly specifies a language or location, their instruction always takes precedence.
### Git clones
- Always clone repos into `workspace/src/<repo-name>`, not directly into workspace root.
### Temporary files ### Temporary files
- All temporary files go to `tmp/` directory. - All temporary files go to `tmp/` directory.
- Clean up after tests and one-off operations. - Clean up after tests and one-off operations.
@@ -63,3 +66,22 @@ something, that is a request for information only. Report your findings, then
ask whether I want them carried out — never treat learning about a problem as a ask whether I want them carried out — never treat learning about a problem as a
request to fix it. When in doubt, ask first. request to fix it. When in doubt, ask first.
## Behavioral Guidelines
1. Don't assume. Don't hide confusion. Surface tradeoffs.
- If an instruction is ambiguous, stop and ask for clarification before acting.
- Do not make assumptions about user intent, data formats, or scope.
- Explicitly surface tradeoffs when multiple implementation paths exist.
2. Minimum code that solves the problem. Nothing speculative.
- Implement only the logic requested. Avoid premature abstraction, design patterns (like Strategy or Factory), or future-proofing that is not explicitly required.
- If a simple solution exists, prefer it over complex, generalized ones.
3. Touch only what you must. Clean up only your own mess.
- Changes must be surgical. Do not reformat files, update type hints, or rewrite existing code unless it is strictly required to fulfill the specific task.
- If your changes introduce orphans (e.g., unused imports, dead variables), clean them up. Otherwise, leave existing code untouched.
4. Define success criteria. Loop until verified.
- Before coding, define clear success criteria or a verification plan.
- Iterate and self-correct until the verification tests pass. Ensure each step of the implementation is verified against the goal.

View File

@@ -36,45 +36,135 @@
} }
}, },
"state": { "state": {
"nextRunAtMs": 1782799200000, "nextRunAtMs": 1784786400000,
"lastRunAtMs": 1782712800001, "lastRunAtMs": 1784700000002,
"lastStatus": "ok", "lastStatus": "ok",
"lastError": null, "lastError": null,
"runHistory": [ "runHistory": [
{ {
"runAtMs": 1782367200027, "runAtMs": 1783058400001,
"status": "ok", "status": "ok",
"durationMs": 5221, "durationMs": 10208,
"error": null "error": null
}, },
{ {
"runAtMs": 1782453600001, "runAtMs": 1783144800002,
"status": "ok", "status": "ok",
"durationMs": 457, "durationMs": 6546,
"error": null "error": null
}, },
{ {
"runAtMs": 1782540000002, "runAtMs": 1783231200001,
"status": "ok", "status": "ok",
"durationMs": 6769, "durationMs": 6447,
"error": null "error": null
}, },
{ {
"runAtMs": 1782626400002, "runAtMs": 1783317600001,
"status": "ok", "status": "ok",
"durationMs": 11179, "durationMs": 7729,
"error": null "error": null
}, },
{ {
"runAtMs": 1782712800001, "runAtMs": 1783404000001,
"status": "ok", "status": "ok",
"durationMs": 4816, "durationMs": 11954,
"error": null
},
{
"runAtMs": 1783490400002,
"status": "ok",
"durationMs": 6695,
"error": null
},
{
"runAtMs": 1783576800001,
"status": "ok",
"durationMs": 7226,
"error": null
},
{
"runAtMs": 1783663200002,
"status": "ok",
"durationMs": 24591,
"error": null
},
{
"runAtMs": 1783749600001,
"status": "ok",
"durationMs": 6103,
"error": null
},
{
"runAtMs": 1783836000002,
"status": "ok",
"durationMs": 13714,
"error": null
},
{
"runAtMs": 1783922400001,
"status": "ok",
"durationMs": 4394,
"error": null
},
{
"runAtMs": 1784008800001,
"status": "ok",
"durationMs": 5509,
"error": null
},
{
"runAtMs": 1784095200002,
"status": "ok",
"durationMs": 9619,
"error": null
},
{
"runAtMs": 1784181600002,
"status": "ok",
"durationMs": 4033,
"error": null
},
{
"runAtMs": 1784268000002,
"status": "ok",
"durationMs": 6314,
"error": null
},
{
"runAtMs": 1784354400012,
"status": "ok",
"durationMs": 5403,
"error": null
},
{
"runAtMs": 1784440800001,
"status": "ok",
"durationMs": 5356,
"error": null
},
{
"runAtMs": 1784527200003,
"status": "ok",
"durationMs": 9145,
"error": null
},
{
"runAtMs": 1784613600002,
"status": "ok",
"durationMs": 6222,
"error": null
},
{
"runAtMs": 1784700000002,
"status": "ok",
"durationMs": 5361,
"error": null "error": null
} }
] ]
}, },
"createdAtMs": 1782281393634, "createdAtMs": 1782281393634,
"updatedAtMs": 1782712804817, "updatedAtMs": 1784700005363,
"deleteAfterRun": false "deleteAfterRun": false
}, },
{ {
@@ -101,135 +191,135 @@
"originMetadata": {} "originMetadata": {}
}, },
"state": { "state": {
"nextRunAtMs": 1782794413934, "nextRunAtMs": 1784718864680,
"lastRunAtMs": 1782787212642, "lastRunAtMs": 1784711663306,
"lastStatus": "ok", "lastStatus": "ok",
"lastError": null, "lastError": null,
"runHistory": [ "runHistory": [
{ {
"runAtMs": 1782650254236, "runAtMs": 1784574764878,
"status": "ok", "status": "ok",
"durationMs": 1314, "durationMs": 1346,
"error": null "error": null
}, },
{ {
"runAtMs": 1782657455552, "runAtMs": 1784581966225,
"status": "ok", "status": "ok",
"durationMs": 1291, "durationMs": 1325,
"error": null "error": null
}, },
{ {
"runAtMs": 1782664656845, "runAtMs": 1784589167553,
"status": "ok", "status": "ok",
"durationMs": 1286, "durationMs": 1340,
"error": null "error": null
}, },
{ {
"runAtMs": 1782671858132, "runAtMs": 1784596368895,
"status": "ok", "status": "ok",
"durationMs": 1310, "durationMs": 1336,
"error": null "error": null
}, },
{ {
"runAtMs": 1782679059443, "runAtMs": 1784603570233,
"status": "ok", "status": "ok",
"durationMs": 1269, "durationMs": 1389,
"error": null "error": null
}, },
{ {
"runAtMs": 1782686260713, "runAtMs": 1784610771624,
"status": "ok", "status": "ok",
"durationMs": 1284, "durationMs": 1322,
"error": null "error": null
}, },
{ {
"runAtMs": 1782693461998, "runAtMs": 1784617972948,
"status": "ok", "status": "ok",
"durationMs": 1281, "durationMs": 12914,
"error": null "error": null
}, },
{ {
"runAtMs": 1782700663280, "runAtMs": 1784625185864,
"status": "ok", "status": "ok",
"durationMs": 1316, "durationMs": 1325,
"error": null "error": null
}, },
{ {
"runAtMs": 1782707864597, "runAtMs": 1784632387190,
"status": "ok", "status": "ok",
"durationMs": 37152, "durationMs": 1341,
"error": null "error": null
}, },
{ {
"runAtMs": 1782715101750, "runAtMs": 1784639588532,
"status": "ok", "status": "ok",
"durationMs": 1273, "durationMs": 1323,
"error": null "error": null
}, },
{ {
"runAtMs": 1782722303024, "runAtMs": 1784646789856,
"status": "ok", "status": "ok",
"durationMs": 1298, "durationMs": 1376,
"error": null "error": null
}, },
{ {
"runAtMs": 1782729504323, "runAtMs": 1784653991234,
"status": "ok", "status": "ok",
"durationMs": 1412, "durationMs": 1344,
"error": null "error": null
}, },
{ {
"runAtMs": 1782736705737, "runAtMs": 1784661192580,
"status": "ok", "status": "ok",
"durationMs": 1274, "durationMs": 1347,
"error": null "error": null
}, },
{ {
"runAtMs": 1782743907012, "runAtMs": 1784668393929,
"status": "ok", "status": "ok",
"durationMs": 1289, "durationMs": 1355,
"error": null "error": null
}, },
{ {
"runAtMs": 1782751108304, "runAtMs": 1784675595285,
"status": "ok", "status": "ok",
"durationMs": 22738, "durationMs": 1378,
"error": null "error": null
}, },
{ {
"runAtMs": 1782758331044, "runAtMs": 1784682796664,
"status": "ok", "status": "ok",
"durationMs": 1309, "durationMs": 1345,
"error": null "error": null
}, },
{ {
"runAtMs": 1782765532355, "runAtMs": 1784689998010,
"status": "ok", "status": "ok",
"durationMs": 77696, "durationMs": 1365,
"error": null "error": null
}, },
{ {
"runAtMs": 1782772810052, "runAtMs": 1784697199383,
"status": "ok", "status": "ok",
"durationMs": 1290, "durationMs": 1362,
"error": null "error": null
}, },
{ {
"runAtMs": 1782780011344, "runAtMs": 1784704400748,
"status": "ok", "status": "ok",
"durationMs": 1295, "durationMs": 62557,
"error": null "error": null
}, },
{ {
"runAtMs": 1782787212642, "runAtMs": 1784711663306,
"status": "ok", "status": "ok",
"durationMs": 1292, "durationMs": 1374,
"error": null "error": null
} }
] ]
}, },
"createdAtMs": 1782369366134, "createdAtMs": 1782900509007,
"updatedAtMs": 1782787213934, "updatedAtMs": 1784711664680,
"deleteAfterRun": false "deleteAfterRun": false
}, },
{ {
@@ -256,179 +346,135 @@
"originMetadata": {} "originMetadata": {}
}, },
"state": { "state": {
"nextRunAtMs": 1782790813940, "nextRunAtMs": 1784717064686,
"lastRunAtMs": 1782789013940, "lastRunAtMs": 1784715264686,
"lastStatus": "ok", "lastStatus": "ok",
"lastError": null, "lastError": null,
"runHistory": [ "runHistory": [
{ {
"runAtMs": 1782754731049, "runAtMs": 1784680996701,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784682798010,
"status": "ok", "status": "ok",
"durationMs": 1, "durationMs": 1,
"error": null "error": null
}, },
{ {
"runAtMs": 1782756531052, "runAtMs": 1784684598011,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782758332355,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782760132356,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782761932356,
"status": "ok", "status": "ok",
"durationMs": 1, "durationMs": 1,
"error": null "error": null
}, },
{ {
"runAtMs": 1782763732358, "runAtMs": 1784686398014,
"status": "ok", "status": "ok",
"durationMs": 1, "durationMs": 1,
"error": null "error": null
}, },
{ {
"runAtMs": 1782765610055, "runAtMs": 1784688198016,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784689999378,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784691799379,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784693599380,
"status": "ok", "status": "ok",
"durationMs": 1, "durationMs": 1,
"error": null "error": null
}, },
{ {
"runAtMs": 1782767410058, "runAtMs": 1784695399383,
"status": "ok", "status": "ok",
"durationMs": 0, "durationMs": 0,
"error": null "error": null
}, },
{ {
"runAtMs": 1782769210059, "runAtMs": 1784697200745,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782771010061,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782772811346,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782774611350,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782776411351,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782778211354,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782780012642,
"status": "ok", "status": "ok",
"durationMs": 1, "durationMs": 1,
"error": null "error": null
}, },
{ {
"runAtMs": 1782781812645, "runAtMs": 1784699000748,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782783612647,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1782785412648,
"status": "ok", "status": "ok",
"durationMs": 1, "durationMs": 1,
"error": null "error": null
}, },
{ {
"runAtMs": 1782787213938, "runAtMs": 1784700800751,
"status": "ok", "status": "ok",
"durationMs": 0, "durationMs": 0,
"error": null "error": null
}, },
{ {
"runAtMs": 1782789013940, "runAtMs": 1784702600757,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784704463308,
"status": "ok",
"durationMs": 1,
"error": null
},
{
"runAtMs": 1784706263311,
"status": "ok",
"durationMs": 1,
"error": null
},
{
"runAtMs": 1784708063313,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784709863314,
"status": "ok",
"durationMs": 1,
"error": null
},
{
"runAtMs": 1784711664683,
"status": "ok",
"durationMs": 0,
"error": null
},
{
"runAtMs": 1784713464684,
"status": "ok",
"durationMs": 1,
"error": null
},
{
"runAtMs": 1784715264686,
"status": "ok", "status": "ok",
"durationMs": 0, "durationMs": 0,
"error": null "error": null
} }
] ]
}, },
"createdAtMs": 1782369366136, "createdAtMs": 1782900509009,
"updatedAtMs": 1782789013940, "updatedAtMs": 1784715264686,
"deleteAfterRun": false
},
{
"id": "69484aa6",
"name": "compact-memory-auto-daily",
"enabled": true,
"schedule": {
"kind": "cron",
"atMs": null,
"everyMs": null,
"expr": "0 2 * * *",
"tz": "Europe/Prague"
},
"payload": {
"kind": "agent_turn",
"message": "Run /compact-memory auto skill to clean up memory/MEMORY.md",
"deliver": false,
"channel": null,
"to": null,
"channelMeta": {},
"sessionKey": "telegram:8826147089",
"originChannel": "telegram",
"originChatId": "8826147089",
"originMetadata": {
"message_id": 1159,
"user_id": 8826147089,
"username": null,
"first_name": "Martin",
"is_group": false,
"message_thread_id": null,
"is_forum": false,
"reply_to_message_id": null,
"_wants_stream": true
}
},
"state": {
"nextRunAtMs": 1782864000000,
"lastRunAtMs": null,
"lastStatus": null,
"lastError": null,
"runHistory": []
},
"createdAtMs": 1782789590878,
"updatedAtMs": 1782789590878,
"deleteAfterRun": false "deleteAfterRun": false
} }
] ]

View File

@@ -1,5 +1,131 @@
# History # History
## 2026-07-02 — bookmark: zestručnění frontmatter description (routing-focused), nasazeno
**Cíl:** Popis skillu byl moc dlouhý, prozaický a obecný („Manage a personal reading list…"). Přepsat na krátký, foldovaný (`>`) a zaměřený na *kdy aktivovat*, ne na výčet funkcí.
**Co jsem zkusil / co fungovalo:** Několik iterací s uživatelem (on sám mezitím editoval v IDE). Finální verze: `Use when the user wants to save a URL (with or without additional content) to read later or search in. Triggers on "bookmark", "save URL".` Cestou opraveny drobnosti (`an URL``a URL`, uzavření vsuvky, rozbité souvětí) a odstraněn zbloudilý `ks` mezi frontmatterem a `# Bookmark`. Detailní výčet příkazů zůstává v těle skillu, kam patří.
**Nasazení:** Server vs lokál se lišil **jen** v description (tělo identické → žádný Dream edit k dotažení). Nahrán `rsync` **jen `SKILL.md`** (ne celý adresář — `bookmark.py` a `html_to_markdown.py` jsou lokálně rozdělané a nasazení nebylo požadováno). Owner `nanobot:nanobot` ✓. Skill se čte per-tah bez restartu.
**Co zbývá:** Rozdělané změny skriptů (`bookmark.py`, `html_to_markdown.py`, testy) — nenasazeno, čeká na dokončení.
**Jak vrátit zpět:** `git checkout HEAD -- skills/bookmark/SKILL.md` (po commitu příslušný `git revert`), pak `rsync` na server.
## 2026-06-17 — llm-wiki: oprava reference souborů z code-review (10 nálezů)
**Cíl:** Vykonat schválený plán oprav reference souborů skillu (review byl zaměřený na `references/`). Nálezy: bare cesty + kroky workflow předpokládající interaktivního uživatele navzdory headless compile (cron drain).
**Co jsem zkusil / klíčový vhled:** SKILL.md ř. 22 má globální override „kdekoli docs říkají `wiki/`, čti `cml/wiki/`" — takže bare-`wiki/` cesty v referencích nejsou rozbité, jen přesměrované. Většina path-nálezů z review tím padá jako ne-bug. Reálné bugy jsou ty, které override nezachrání: kroky „discuss/triage with the user" (v drainu není uživatel → uvázne nebo tiše přeskočí, graf zastará) a „regenerate graph first" v query tahu (compile práce v interaktivním tahu). Server vs repo `diff` před i po prázdný (žádný Dream edit). Vykonání delegováno na sonnet agenta dle konvence repa; sweep cest udělán boundary-aware perlem (`(?<![-/\w.])wiki/`), aby nerozbil `llm-wiki/` ani `wiki_*.py`.
**Co fungovalo a proč:** Skupina A (4 fázové bugy): ingest Step 3 → interactive-only + drain skip; ingest Step 8b + graph Step 1 → Interactive/Drain větve (lint čistý→extract, chyby→log+skip, neblokovat); query Step 2b + graph ř. 106 → graf v query neregenerovat; divergentní skip podmínky sjednoceny na `graph:` metadata v obou souborech. Skupina B: na žádost uživatele plný sweep `wiki/``cml/wiki/`, `raw/``cml/raw/` (literál bezpečnější než spoléhat na override; reference byly stejně už půl-převedené) — `skills/llm-wiki/scripts/` netknuté, ověřeno grepem. Skupina C: smazán `references/agent-memory-integration.md` (SKILL.md ho na nanobotu stejně přeskakuje — wiki se nedrátuje do agent-memory) + úklid 2 odkazů v SKILL.md; odstraněna ne-akční sekce „Why this architecture" z architecture.md. Nasazeno `rsync` (smazaný soubor řešen explicitním `ssh rm`, ne `--delete`), owner `nanobot:nanobot` ✓, server-side spot-check čistý.
**Co zbývá:** Volitelný smoke test drainu na serveru (1 testovací zdroj — že neuvázne na „discuss with user"). 4 follow-up zlepšení (akční lint data, frontmatter šablona, narrow `--fix`, slug kolize) zůstávají v [todo.md](todo.md).
**Commit:** `c74e7de`. **Jak vrátit zpět:** `git revert c74e7de` (nebo `git checkout c74e7de~1 -- skills/llm-wiki/`), pak `rsync` na server + obnovit smazaný soubor z gitu.
## 2026-06-17 — llm-wiki: tvrdá report-only brána u lintu (lint tah se zacyklil 24 min)
**Cíl:** Uživatel hlásil, že lint nad wiki je „šíleně pomalý", zasekl se hned napoprvé, točil se 24 min a sežral hafo tokenů. Najít příčinu (bez opravy), pak opravit.
**Co jsem zkusil / příčina:** Z transkriptu: lint spustil `wiki_graph_lint.py` (nahlásil ~2030 chyb), ale místo report→STOP spadl do plného opravného režimu — 7 nových stránek, 30+ editací, debug skripty do `tmp/`, regenerace grafu, re-lint ve smyčce; `wiki_graph_lint.py` přečetl ~15× po sobě (vlastní slova: „četl jsem ty samý soubory dokola"). Příčiny: **(A)** „Lint is report-only" byla jediná měkká odrážka v SKILL.md, kterou referenční `lint-workflow.md` přímo popírala — má 6krokový postup celý o mutaci wiki (Step 2 fix, Step 5 index, Step 6 log), strukturálně čte jako „oprav vše a zaloguj". Stejná chyba jako capture/compile brána (history 16.6. 22:45). **(B)** Žádný strop na opravnou/debug smyčku; graph lint je provázaný (oprava jedné hrany vyrobí orphan) → ruční edity ho těžko uspokojí → smyčka edit↔lint. **(C)** Skript sám rychlý (0,150,26 s na téhle wiki) — 24 min byla 100 % agentní smyčka, ne CLI.
**Co fungovalo a proč:** Server=zdroj pravdy: `rsync` server→`tmp/`, diff `SKILL.md`+`references/lint-workflow.md` = IDENTICAL (žádný Dream edit). Fix (recept z capture brány): (1) tvrdá report-only brána v SKILL.md ř.26 — lint tah = spusť 2 skripty, předej report, **STOP** + výčet zakázaných akcí (žádné stránky, debug skripty, regenerace grafu, re-lint smyčka); (2) „lint workflow (summary)" přerámován na report-first + STOP-redirect + strop na opravnou smyčku (bounded batches, necti skript opakovaně, re-lint max 1× na ověření, pak ptej se); (3) `lint-workflow.md` dostal report-only banner nahoře + mutační kroky (2/5/6) označeny „fix turn — only after approval". Nasazeno `rsync` (přenesly se jen 2 soubory), owner `nanobot:nanobot` ✓. Skill se čte per-tah bez restartu.
**Co zbývá:** Behaviorální ověření dělá uživatel — příští `lint` tah musí jen nahlásit a zastavit. Vyhodnotit 7 follow-up návrhů nanobota (--fix mód, akční lint data, frontmatter šablona, slug kolize při add, timeout per krok).
**Jak vrátit zpět:** `git checkout HEAD~1 -- skills/llm-wiki/SKILL.md skills/llm-wiki/references/lint-workflow.md`, pak `rsync` na server.
## 2026-06-17 — llm-wiki: code review + oprava 4 chyb v capture bráně + zeštíhlení
**Cíl:** Code review skillu llm-wiki (correctness + verbosity). Uživatel požádal opravit vše.
**Co jsem zkusil / co fungovalo:**
- Review odhalil 4 correctness chyby a 6 verbosity problémů.
- Correctness opravy: (1) ⚠️ mega-odstavec zkrácen na 1-větový pointer — odstranil zdroj rozporů s řádky 38/39; (2) URL při capture: "fetch nebo zapiš" → vždy zapiš as-is; (3) idempotence "pages already exist" zpřesněno na konkrétní soubor `cml/wiki/sources/<slug>.md`; (4) escape hatch triggers nyní jen na jednom místě.
- Verbosity: odstraněny sekce "The pattern in one paragraph" a "When to use this skill" (duplikát frontmatter); directory tree opraven na reálné `cml/wiki/`/`cml/raw/` cesty; "Failure modes" ze 4 odstavců na 2 odrážky; "Bundled scripts" katalog odstraněn (wiki_stats přesunut do scalability).
- Výsledek: 198 → 169 řádků (17 %). Nasazeno na server.
**Commit:** `239dc4f`
## 2026-06-17 05:15 — llm-wiki: zeštíhlit `description` na čistý trigger (+ folded ≤100 zn.)
**Cíl:** `description` ve frontmatteru [SKILL.md](skills/llm-wiki/SKILL.md) byl ukecaný (~1060 zn.) — míchal *kdy spustit* (routing signál) s *jak funguje* (výklad). Dvě kola: (1) zalomit na ≤100 zn., (2) na žádost uživatele zeštíhlit obsah na čistý trigger.
**Mechanika (vysvětleno uživateli):** Do baseline kontextu jde jen jednořádkový `description` (routing signál — `nanobot/agent/skills.py`, viz `knowledge.md` „Skill description — progressive loading"). Tělo SKILL.md se čte on-demand při použití skillu; `references/*.md` nezná ani skill loader — jsou to obyčejné soubory čtené po jednom přes `read_file`, jen když na ně tělo navede. Rozsah referencí je proto kontextově fakticky zadarmo → osekávat se mají kvůli srozumitelnosti, ne tokenům.
**Co jsem udělal:**
- Block scalar `|` (literal — zachoval by vložené `\n`) → `>` (folded — řádky se složí na mezery), zalomeno na ≤100 zn. Folded scalar = v seznamu skillů se renderuje jako jeden řádek; v 1. kole ověřeno `yaml.safe_load`, že načtená hodnota je bajt po bajtu identická.
- Zeštíhlení obsahu (~1060 → ~480 zn.): vyhozeno (a) atribuce + datum („from Andrej Karpathy's April 2026 gist", „OmegaWiki") — origin, nulová routing hodnota, Karpathy už je v těle (SKILL.md:9); (b) how-it-works věta o škálování — už v sekci *The scalability discipline*; (c) dlouhý literální výčet trigger frází — redundantní (`knowledge.md` 2026-05-31: model pozná záměr napříč formulacemi). Ponecháno: co dělá + kdy (ingest/ask/lint/init), „i když uživatel neřekne wiki", a rozlišení vůči agent paměti (`note`/`keep`/`MEMORY.md`) — to má reálnou routing hodnotu. Tělo SKILL.md beze změny.
**Verifikace + deploy:** Server=zdroj pravdy: před každým kolem `rsync` server→`tmp/`, diff → IDENTICAL (Dream nesahal). Po deployi `rsync` zpět + diff → IDENTICAL, owner `nanobot:nanobot` OK. Skilly se čtou bez restartu.
**Jak vrátit zpět:** `git revert <commit>` + `rsync` předchozí verze SKILL.md na server. Zalomení je funkčně no-op; zeštíhlení mění obsah triggeru (užší, ale sémanticky pokrývá totéž).
## 2026-06-14 14:30 — note: tag u multi-line poznámky na header + spolknout osiřelý oddělovač
**Cíl:** V `/note list` (CLI/`nanobot agent`) u poznámky, jejíž URL `_urls_on_own_lines` rozhází na vlastní odrážky, padal tag za poslední URL odrážku (vizuálně nesmysl) a na konci headeru visel osiřelý oddělovač `—` + na první odrážce koncová čárka — pozůstatky inline formy (`… od vína — Universal: url, Amber: url`) po přesunu URL na odrážky.
**Co jsem udělal** ([skills/note/scripts/note.py](skills/note/scripts/note.py)):
- `_LABELED_URL_RE`: prefix `\s*` → třída oddělovačů `[\s,;—–-]*`, takže připojený konektor před URL/labelem padne do nezachyceného prefixu a zahodí se. Mizí osiřelý `—` i `,` mezi URL. DB i `cmd_show` netknuté (drží původní obsah verbatim).
- `cmd_list`: tag vkládám za **první řádek** (`partition("\n")``head` + tag + `sep` + `rest`) místo na konec celého bloku. Jednořádkové poznámky beze změny (`partition` vrátí `(text,"","")`).
**Verifikace + deploy:** Lokální test helperů na konkrétní poznámce (DT GLASS) + jednořádkové → OK. Server=zdroj pravdy: `rsync` server→`tmp/note.server.py`, diff proti lokálu → IDENTICAL (Dream nesahal) → deploy bezpečný. `rsync -av skills/note/` na server, owner `nanobot:nanobot` OK. Reálný `uv run … list` na serveru (přes `~/.local/bin/uv`, non-login PATH gotcha) → položka 7 má tag na headeru, čisté odrážky bez `—`/čárky, ostatní beze změny.
**Souvislost:** Čistě textový výstup skriptu. Nevyřešený WebUI card-renderer problém (`plans/note-wrong-urls.md`) tím není dotčen.
**Jak vrátit zpět:** `git revert <commit>` skillu (zatím necommitnuto) + `rsync` předchozí verze na server. DB beze změny.
## 2026-06-13 07:45 — note: posílit `show` k doslovnému plnému výpisu
**Cíl:** Agent při `/note show` zkracoval výstup a vypouštěl odkaz/URL z obsahu — defaultní terseness ze `SOUL.md` přebíjela slabou instrukci `echo the structured block unchanged`. Skript vypisuje plný blok správně, problém byl jen v prezentaci.
**Co jsem udělal:** V [SKILL.md](skills/note/SKILL.md) `## Show protocol` přepsal bod Exit 0 na emfatickou direktivu: *output the script's stdout verbatim — print every line exactly as emitted; do not summarize, shorten, rewrap, or drop any part of `content` including URLs/links; brevity directives do not apply here.* Žádná změna kódu skriptu.
**Verifikace + deploy:** `diff` serverové SKILL.md proti lokálu → jediný rozdíl moje nová úprava (server jinak nezměněn) → deploy bezpečný. `rsync` SKILL.md na server, owner `nanobot:nanobot` OK. Skilly se čtou bez restartu. Reálné ověření chování agenta na URL poznámce zbývá uživateli.
**Jak vrátit zpět:** `git revert <commit>` + `rsync` předchozí verze SKILL.md na server.
## 2026-06-13 07:40 — note: nový subcommand `show` (jedna poznámka v plném znění)
**Cíl:** `/note` uměl `add`/`list`/`delete`, ale ne vytáhnout **jednu poznámku se vším uloženým**. Přidat `show <display-id>`, který vypíše všechna pole (display ID, DB id, datum, tagy, obsah) ve formě vhodné pro LLM.
**Co jsem zkusil / udělal:**
- **Backend** ([skills/note/scripts/note.py](skills/note/scripts/note.py)): `cmd_show` — mapování display ID → DB id přes existující `_active_ids` (stejná logika jako `cmd_delete`), guard na rozsah s exit 1 + hláškou `No active note with display id=N.`. SELECT přidává `created_at` (delete ho nečte). Tiskne strukturovaný blok `Note [#N] (id=…)` / `created:` (ISO 8601 UTC beze změny) / `tags:` (`#tag` mezerou, jinak `(none)`) / `content:`. Log `SHOW` konzistentně s `LIST`. Subparser `show` + dispatch větev.
- **SKILL.md** ([skills/note/SKILL.md](skills/note/SKILL.md)): sekce `## Show protocol` (triggery, read-only, display IDs jako u list/delete) + edge case `/note show` bez ID.
- **Verifikace lokálně:** add (s tagy / bez) → list → `show` u obou (tagovaná i `tags: (none)`) → `show 999` exit 1 → delete úklid. Vše OK.
- **Server = zdroj pravdy:** `rsync` server→`tmp/note-server/`, `diff` SKILL.md i note.py → jediný rozdíl jsou moje nové `show` přírůstky, žádná autonomní divergence (Dream nesahal) → deploy bezpečný.
- **Deploy:** `rsync -av skills/note/` na server, owner `nanobot:nanobot` OK. Smoke `show 999` na serveru přes plnou cestu `/home/nanobot/.local/bin/uv` (interaktivní `uv` v non-login SSH PATH chybí — známý gotcha) → exit 1, OK.
**Co fungovalo a proč:** Skilly se čtou bez restartu. `show` je čistě čtecí — žádný dopad na DB ani delete logiku. Reuse `_active_ids` znamená, že display IDs jsou konzistentní napříč list/show/delete.
**Co zbývá:** Commit (tracking + skill zvlášť dle konvence).
**Jak vrátit zpět:** `git revert <commit>` skillu + `rsync` předchozí verze na server. DB beze změny.
## 2026-06-10 10:47 — remind: nový subcommand `upcoming` (co a kdy odejde)
**Cíl:** `/remind` neuměl dopředu říct, co a kdy odejde. Výpočet kdy reminder vystřelí už existoval (`remind_send.py` `_due_*`), šlo o jeho vystavení jako dotazu — **bez nafouknutí `remind_cli.py`** (491 řádků).
**Co jsem udělal:**
- Nový sdílený modul `scripts/forecast.py``window_for()`, `fires_in_window()`, `format_upcoming()`. Veškerá výpočetní + formátovací logika sem; CLI dostalo jen tenký handler `cmd_upcoming`.
- Znovupoužity hotové primitivy: `compute_fire_times` (random), `croniter.get_next` (cron), ISO string compare (at). Forecast je čistá projekce rozvrhu — **bez** dedupu proti `reminder_fires` a bez tolerance (na rozdíl od `delivered`, který ukazuje skutečnost).
- DRY: sestavení cfg z `schedule_random` řádku vytaženo do `random_times.random_cfg_from_row()`, použito ve `forecast` i `remind_send` (daemon jinak netknutý).
- Rozhraní: `upcoming` (= zbytek dneška), `--date YYYY-MM-DD` (celý den), `--days N` (N kalendářních dní vč. dneška); `--date`+`--days` se vylučují.
- SKILL.md: řádek do NL→command tabulky + věta do behavioral contractu.
- Testy `tests/test_forecast.py` (16 testů: window_for, at/cron/random, filtry, řazení, formát).
**Co fungovalo a proč:** Lokálně 46 testů green (`uv run --with pytest --with croniter pytest`). Smoke test CLI proti temp DB i mutual-exclusion chyba OK. Nasazeno na server `rsync -av` (vlastník `nanobot:nanobot`), `upcoming --days 2` proti ostré DB vypsalo korektní rozvrh.
**Jak vrátit zpět:** `git revert` příslušného kódového commitu; na serveru smazat `scripts/forecast.py` a `rsync` předchozí verze `random_times.py`/`remind_cli.py`/`remind_send.py`/`SKILL.md`.
**Pozn.:** `uv` na serveru není v non-interaktivním SSH PATH — je v `~/.local/bin/uv` (cron používá shebang `#!/usr/bin/env -S uv run --script`).
## 2026-06-07 19:57 — imageGeneration: provider z openrouter na ollama (žádný paid path) ## 2026-06-07 19:57 — imageGeneration: provider z openrouter na ollama (žádný paid path)
**Cíl:** `tools.imageGeneration` v serverovém configu odkazoval na `openai/gpt-5.4-image-2` přes provider `openrouter` (placený). Uživatel nechce, aby cokoli kolem obrázků šlo přes openrouter — jen ollama provider / cloud modely. **Cíl:** `tools.imageGeneration` v serverovém configu odkazoval na `openai/gpt-5.4-image-2` přes provider `openrouter` (placený). Uživatel nechce, aby cokoli kolem obrázků šlo přes openrouter — jen ollama provider / cloud modely.
@@ -62,6 +188,7 @@
**Co jsem zkusil:** Přidat display ID jako druhé ID pro manipulaci — 1..N mezi aktivními poznámkami, nejnovější = 1, přepočítá se po každém smazání. **Co jsem zkusil:** Přidat display ID jako druhé ID pro manipulaci — 1..N mezi aktivními poznámkami, nejnovější = 1, přepočítá se po každém smazání.
**Co fungovalo a proč:** **Co fungovalo a proč:**
- `_active_ids(conn)` vrátí seznam interních ID v pořadí `created_at DESC` - `_active_ids(conn)` vrátí seznam interních ID v pořadí `created_at DESC`
- `cmd_list` builduje `id_to_display` dict ze stejného pořadí → globálně konzistentní i při `--tag` filtru a `--offset` stránkování - `cmd_list` builduje `id_to_display` dict ze stejného pořadí → globálně konzistentní i při `--tag` filtru a `--offset` stránkování
- `cmd_delete` resolvuje display ID na interní ID přes `_active_ids`, log zaznamenává obě ID - `cmd_delete` resolvuje display ID na interní ID přes `_active_ids`, log zaznamenává obě ID
@@ -178,11 +305,13 @@
**Cíl:** Zprovoznit `npx` (a tím clawhub CLI) na serveru nanobot.hell. **Cíl:** Zprovoznit `npx` (a tím clawhub CLI) na serveru nanobot.hell.
**Co jsme zkusili:** **Co jsme zkusili:**
- Instalace nvm pro uživatele `nanobot`: `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash` - Instalace nvm pro uživatele `nanobot`: `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash`
- Instalace Node.js: `nvm install --lts` → nainstaloval se v24.16.0 do `/home/nanobot/.nvm/versions/node/v24.16.0/` - Instalace Node.js: `nvm install --lts` → nainstaloval se v24.16.0 do `/home/nanobot/.nvm/versions/node/v24.16.0/`
- nvm přidal inicializaci do `.bashrc` — to funguje v interaktivním shellu, ale systemd user service `.bashrc` nesourcuje - nvm přidal inicializaci do `.bashrc` — to funguje v interaktivním shellu, ale systemd user service `.bashrc` nesourcuje
**Co fungovalo a proč:** **Co fungovalo a proč:**
- Přidáno `Environment=PATH=/home/nanobot/.nvm/versions/node/v24.16.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin` do `/home/nanobot/.config/systemd/user/nanobot.service` - Přidáno `Environment=PATH=/home/nanobot/.nvm/versions/node/v24.16.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin` do `/home/nanobot/.config/systemd/user/nanobot.service`
- Reload + restart přes `ssh nanobot@nanobot.hell "systemctl --user daemon-reload && systemctl --user restart nanobot"` (root přes sudo/machine nefunguje, přímé SSH jako nanobot ano) - Reload + restart přes `ssh nanobot@nanobot.hell "systemctl --user daemon-reload && systemctl --user restart nanobot"` (root přes sudo/machine nefunguje, přímé SSH jako nanobot ano)
- Ověřeno: `/proc/<pid>/environ` obsahuje správnou PATH s nvm cestou - Ověřeno: `/proc/<pid>/environ` obsahuje správnou PATH s nvm cestou
@@ -196,6 +325,7 @@
**Cíl:** Přidat do AGENTS.md instrukci, že agent má SQLite databáze ukládat do `db/*.sqlite`. **Cíl:** Přidat do AGENTS.md instrukci, že agent má SQLite databáze ukládat do `db/*.sqlite`.
**Co jsem zkusil / udělal:** **Co jsem zkusil / udělal:**
1. Zkoumal, kde žijí instrukce agenta — SSH na server, prošel `/home/nanobot/.nanobot/workspace/`. 1. Zkoumal, kde žijí instrukce agenta — SSH na server, prošel `/home/nanobot/.nanobot/workspace/`.
2. Zjistil, že správné místo je `AGENTS.md` (operační pravidla), ne `SOUL.md` (osobnost) ani skills. 2. Zjistil, že správné místo je `AGENTS.md` (operační pravidla), ne `SOUL.md` (osobnost) ani skills.
3. Přidal sekci `## Databases (SQLite)` do `workspace/AGENTS.md` (anglicky) a pushnu na server. 3. Přidal sekci `## Databases (SQLite)` do `workspace/AGENTS.md` (anglicky) a pushnu na server.
@@ -268,19 +398,23 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
3. **Knowledge.md doplněn** o dvě nové sekce: „Co se auto-loaduje do system promptu (verze 0.2.0)" + tabulka „K čemu slouží jednotlivé workspace soubory" (SOUL/AGENTS/USER/TOOLS/MEMORY/HEARTBEAT). Stará minimální sekce „Workspace bootstrap soubory se čtou každý tah" nahrazena bohatší variantou; sekce „Workspace vzniká při prvním spuštění agenta" ponechána beze změny. 3. **Knowledge.md doplněn** o dvě nové sekce: „Co se auto-loaduje do system promptu (verze 0.2.0)" + tabulka „K čemu slouží jednotlivé workspace soubory" (SOUL/AGENTS/USER/TOOLS/MEMORY/HEARTBEAT). Stará minimální sekce „Workspace bootstrap soubory se čtou každý tah" nahrazena bohatší variantou; sekce „Workspace vzniká při prvním spuštění agenta" ponechána beze změny.
**Proč SOUL.md a ne AGENTS.md / skill:** **Proč SOUL.md a ne AGENTS.md / skill:**
- AGENTS.md drží procesní pravidla (jaký tool kdy) — pravdomluvnost je hodnota, ne proces. - AGENTS.md drží procesní pravidla (jaký tool kdy) — pravdomluvnost je hodnota, ne proces.
- Always-on skill pro jedno pravidlo je overkill. Skilly dávají smysl, když potřebuje izolaci (own helper scripts, modulární distribuce). Sémanticky horší než SOUL pro hodnotové pravidlo. - Always-on skill pro jedno pravidlo je overkill. Skilly dávají smysl, když potřebuje izolaci (own helper scripts, modulární distribuce). Sémanticky horší než SOUL pro hodnotové pravidlo.
- MEMORY.md by Dream při konsolidaci mohl přepsat — nehodí se na trvalá pravidla chování. - MEMORY.md by Dream při konsolidaci mohl přepsat — nehodí se na trvalá pravidla chování.
**Co jsem zkusil mimo úspěšnou cestu:** **Co jsem zkusil mimo úspěšnou cestu:**
- Explore agent na začátku trval na konkrétních line numbers v `/tmp/nanobot-src/...` ještě před tím, než tam upstream byl klonovaný — fabulace zdroje, kterou jsem musel přebít přímým ověřením. Jasná ilustrace toho, proč user chce pravdomluvnost: i podagent halucinoval citace zdroje. Pravidlo „neopakuj číslo řádku, dokud jsi ho neviděl" by se hodilo i na můj vlastní workflow, ne jen na nanobota. - Explore agent na začátku trval na konkrétních line numbers v `/tmp/nanobot-src/...` ještě před tím, než tam upstream byl klonovaný — fabulace zdroje, kterou jsem musel přebít přímým ověřením. Jasná ilustrace toho, proč user chce pravdomluvnost: i podagent halucinoval citace zdroje. Pravidlo „neopakuj číslo řádku, dokud jsi ho neviděl" by se hodilo i na můj vlastní workflow, ne jen na nanobota.
- Místně bylo `BOOTSTRAP_FILES = [AGENTS, SOUL, USER]` v upstream HEAD — málem jsem usoudil, že `TOOLS.md` se nečte. Ověření verze na serveru (`nanobot --version` → 0.2.0, `grep BOOTSTRAP_FILES` v nainstalovaném wheelu) odhalilo divergenci verzí. - Místně bylo `BOOTSTRAP_FILES = [AGENTS, SOUL, USER]` v upstream HEAD — málem jsem usoudil, že `TOOLS.md` se nečte. Ověření verze na serveru (`nanobot --version` → 0.2.0, `grep BOOTSTRAP_FILES` v nainstalovaném wheelu) odhalilo divergenci verzí.
**Jak to vrátit zpět:** **Jak to vrátit zpět:**
- SOUL.md: `ssh nanobot@nanobot.hell 'python3 -c "from pathlib import Path; p=Path.home()/\".nanobot/workspace/SOUL.md\"; t=p.read_text(); import re; p.write_text(re.sub(r\"## Faktografická pravidla.*?(?=## Styl výstupu)\", \"\", t, flags=re.S))"'` - SOUL.md: `ssh nanobot@nanobot.hell 'python3 -c "from pathlib import Path; p=Path.home()/\".nanobot/workspace/SOUL.md\"; t=p.read_text(); import re; p.write_text(re.sub(r\"## Faktografická pravidla.*?(?=## Styl výstupu)\", \"\", t, flags=re.S))"'`
- film_policy.md: obnovit z paměti (krátký, obsah je teď v history.md SOUL.md patchi nebo v původní git komitové historii local repo, pokud byl). - film_policy.md: obnovit z paměti (krátký, obsah je teď v history.md SOUL.md patchi nebo v původní git komitové historii local repo, pokud byl).
**Co zbývá:** **Co zbývá:**
- Live verifikace v Telegramu — dotaz na film, sledování `journalctl -u nanobot.service -f` na `web_search`/`web_fetch` tool call před odpovědí. Cross-test s netriviálním dotazem (ověřit „nevím" místo improvizace). Negativní kontrola s in-context dotazem (např. „jaký mám preset?") — nesmí spustit zbytečné web search. - Live verifikace v Telegramu — dotaz na film, sledování `journalctl -u nanobot.service -f` na `web_search`/`web_fetch` tool call před odpovědí. Cross-test s netriviálním dotazem (ověřit „nevím" místo improvizace). Negativní kontrola s in-context dotazem (např. „jaký mám preset?") — nesmí spustit zbytečné web search.
--- ---
@@ -290,12 +424,14 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
**Cíl:** Ověřit, jestli Claude Code skilly (Anthropic Skills format) jdou pustit v nanobotovi bez konverze. Jako kanárek vybrán `grill-me` z [nicecode/plugins/mattpocock](https://github.com/lachtan/nicecode/tree/master/plugins/mattpocock) — nejmenší možná povrchová plocha (10 řádků, žádné helper skripty, žádné `@`-reference, žádné Claude-Code-specific tooly v těle). **Cíl:** Ověřit, jestli Claude Code skilly (Anthropic Skills format) jdou pustit v nanobotovi bez konverze. Jako kanárek vybrán `grill-me` z [nicecode/plugins/mattpocock](https://github.com/lachtan/nicecode/tree/master/plugins/mattpocock) — nejmenší možná povrchová plocha (10 řádků, žádné helper skripty, žádné `@`-reference, žádné Claude-Code-specific tooly v těle).
**Co fungovalo:** **Co fungovalo:**
- Formát `SKILL.md` (YAML `name`+`description` + markdown tělo) je identický s nanobot skill formátem. Kopie 1:1, žádná konverze. - Formát `SKILL.md` (YAML `name`+`description` + markdown tělo) je identický s nanobot skill formátem. Kopie 1:1, žádná konverze.
- Deploy přímo na server jedním SSH+curl one-linerem (`ssh nanobot@nanobot.hell``mkdir -p ~/.nanobot/workspace/skills/grill-me``curl -sSfL <raw github URL> -o SKILL.md`). Server-to-GitHub, lokálně se nic neukládá. - Deploy přímo na server jedním SSH+curl one-linerem (`ssh nanobot@nanobot.hell``mkdir -p ~/.nanobot/workspace/skills/grill-me``curl -sSfL <raw github URL> -o SKILL.md`). Server-to-GitHub, lokálně se nic neukládá.
- Restart nanobot.service není potřeba — bootstrap soubory ve `workspace/skills/` se čtou každý tah ([knowledge.md "Workspace bootstrap soubory se čtou každý tah"](knowledge.md)). - Restart nanobot.service není potřeba — bootstrap soubory ve `workspace/skills/` se čtou každý tah ([knowledge.md "Workspace bootstrap soubory se čtou každý tah"](knowledge.md)).
- Uživatel ověřil v chatu: agent na trigger frázi začal pokládat otázky jednu po druhé s recommended answers, místo aby plán rovnou implementoval. Chování přesně podle popisu skillu. - Uživatel ověřil v chatu: agent na trigger frázi začal pokládat otázky jednu po druhé s recommended answers, místo aby plán rovnou implementoval. Chování přesně podle popisu skillu.
**Co jsem zkusil mimo úspěšnou cestu:** **Co jsem zkusil mimo úspěšnou cestu:**
- WebFetch na `diagnose/SKILL.md` při průzkumu vrátil výstup s **fake `<system-reminder>` tagem** uvnitř markdown těla (prompt-injection patrně v Anthropic-fetch pipeline). Zaznamenáno v knowledge.md jako gotcha pro budoucí mattpocock skilly — markdown těla je nutné prohlédnout před deployem, protože nanobot je čte do system contextu. - WebFetch na `diagnose/SKILL.md` při průzkumu vrátil výstup s **fake `<system-reminder>` tagem** uvnitř markdown těla (prompt-injection patrně v Anthropic-fetch pipeline). Zaznamenáno v knowledge.md jako gotcha pro budoucí mattpocock skilly — markdown těla je nutné prohlédnout před deployem, protože nanobot je čte do system contextu.
- `WebFetch` celkově nedával užitečné odpovědi na otázky typu „obsahuje tento soubor X?" — vracel jen echo dotazu. Přepnul jsem na `curl | grep` přes Bash a dostal jednoznačné odpovědi. - `WebFetch` celkově nedával užitečné odpovědi na otázky typu „obsahuje tento soubor X?" — vracel jen echo dotazu. Přepnul jsem na `curl | grep` přes Bash a dostal jednoznačné odpovědi.
@@ -312,6 +448,7 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
**Cíl:** Logy `/remind` skiluu byly v rootu workspace (smetí v hlavním adresáři), formát byl `ISO+02:00 \t text` (tabulátor, timezone suffix nechtěný). **Cíl:** Logy `/remind` skiluu byly v rootu workspace (smetí v hlavním adresáři), formát byl `ISO+02:00 \t text` (tabulátor, timezone suffix nechtěný).
**Co fungovalo:** **Co fungovalo:**
- `LOG_DIR = WORKSPACE / "log"` v `skills/remind/scripts/remind_send.py`, `LOG_DIR.mkdir(parents=True, exist_ok=True)` před zápisem. - `LOG_DIR = WORKSPACE / "log"` v `skills/remind/scripts/remind_send.py`, `LOG_DIR.mkdir(parents=True, exist_ok=True)` před zápisem.
- Formát změněn na `datetime.now(TZ).replace(tzinfo=None).isoformat(timespec="seconds")` + mezera + text + `\n``YYYY-MM-DDTHH:MM:SS <text>`. TZ zůstává Prague, jen se nepíše do řetězce. - Formát změněn na `datetime.now(TZ).replace(tzinfo=None).isoformat(timespec="seconds")` + mezera + text + `\n``YYYY-MM-DDTHH:MM:SS <text>`. TZ zůstává Prague, jen se nepíše do řetězce.
- Crontab pod nanobot userem přepsán: `>> /home/nanobot/.nanobot/workspace/log/reminder_cron.log 2>&1`. Soubor `reminder_cron.log` zůstává — chytá stdout/stderr crontabu pro debug (za zdravého běhu prázdný). - Crontab pod nanobot userem přepsán: `>> /home/nanobot/.nanobot/workspace/log/reminder_cron.log 2>&1`. Soubor `reminder_cron.log` zůstává — chytá stdout/stderr crontabu pro debug (za zdravého běhu prázdný).
@@ -322,6 +459,7 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
**Proč dva logy:** `log/reminder.log` = aplikační (úspěšná doručení, čte agent). `log/reminder_cron.log` = zachytávka stdout/stderr crontabu (`>> ... 2>&1`) — bezpečnostní síť pro tracebacky / chyby uv. Funkce se nepřekrývají. **Proč dva logy:** `log/reminder.log` = aplikační (úspěšná doručení, čte agent). `log/reminder_cron.log` = zachytávka stdout/stderr crontabu (`>> ... 2>&1`) — bezpečnostní síť pro tracebacky / chyby uv. Funkce se nepřekrývají.
**Co selhalo / zákoutí:** **Co selhalo / zákoutí:**
- První patch `remind_send.py` jsem zkusil pod `sudo -u nanobot``PermissionError`. Soubor byl owned `root:root` (vytvořen v předchozí session přes SSH jako root). Fix: patch jako root + `chown nanobot:nanobot` po editaci. - První patch `remind_send.py` jsem zkusil pod `sudo -u nanobot``PermissionError`. Soubor byl owned `root:root` (vytvořen v předchozí session přes SSH jako root). Fix: patch jako root + `chown nanobot:nanobot` po editaci.
- Reformat stávajícího `reminder.log` přes „chytrý" Python jednořádkový regex (`ts.split("+")[0].split("-0")[0]`) zmrvil obsah na samé `2026 <text>``-0` matchlo už `-05` v datu. Fix: přepsat soubor staticky známým obsahem (8 řádků jsem stejně měl před očima z předchozího `cat`). - Reformat stávajícího `reminder.log` přes „chytrý" Python jednořádkový regex (`ts.split("+")[0].split("-0")[0]`) zmrvil obsah na samé `2026 <text>``-0` matchlo už `-05` v datu. Fix: přepsat soubor staticky známým obsahem (8 řádků jsem stejně měl před očima z předchozího `cat`).
- Manuální dry-run `sudo -u nanobot uv run …` selhal dvakrát: nejdřív `uv: command not found` (PATH bez `~/.local/bin`), pak `failed to open /root/uv.toml` (cwd zůstal `/root`). Funguje až `cd /home/nanobot && sudo -Hu nanobot bash -lc "uv run …"`. Cron tohle neřeší — má vlastní `HOME` a `PATH` v hlavičce crontabu a běží z domovského adresáře nanobota. - Manuální dry-run `sudo -u nanobot uv run …` selhal dvakrát: nejdřív `uv: command not found` (PATH bez `~/.local/bin`), pak `failed to open /root/uv.toml` (cwd zůstal `/root`). Funguje až `cd /home/nanobot && sudo -Hu nanobot bash -lc "uv run …"`. Cron tohle neřeší — má vlastní `HOME` a `PATH` v hlavičce crontabu a běží z domovského adresáře nanobota.
@@ -335,12 +473,14 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
**Cíl:** Pochopit, proč chodí na Telegram zprávy "(Output was empty — no message sent.)" / "(Výstup byl prázdný…)" — a ne každou minutu, ale jen sem tam a pokaždé jinak formulované. Předchozí fix ("exit silently" v promptu) nestačil. **Cíl:** Pochopit, proč chodí na Telegram zprávy "(Output was empty — no message sent.)" / "(Výstup byl prázdný…)" — a ne každou minutu, ale jen sem tam a pokaždé jinak formulované. Předchozí fix ("exit silently" v promptu) nestačil.
**Skutečná příčina (oprava dřívějšího závěru):** **Skutečná příčina (oprava dřívějšího závěru):**
1. Prompt `"The scheduled time has arrived. Deliver this reminder to the user now…"` **není** z jobs.json — je **natvrdo ve zdrojáku** `nanobot/cli/commands.py:on_cron_job`. `payload.message` z jobs.json se jen přilepí za `Reminder:`. Takže každý cron job dostane konfliktní zadání (kód přikazuje doručit, můj text říká mlčet) → agent improvizuje meta-odpověď. 1. Prompt `"The scheduled time has arrived. Deliver this reminder to the user now…"` **není** z jobs.json — je **natvrdo ve zdrojáku** `nanobot/cli/commands.py:on_cron_job`. `payload.message` z jobs.json se jen přilepí za `Reminder:`. Takže každý cron job dostane konfliktní zadání (kód přikazuje doručit, můj text říká mlčet) → agent improvizuje meta-odpověď.
2. Cron job VŽDY jede přes agenta (`agent.process_direct`) — neagentní typ jobu neexistuje. Agent komentuje své mlčení místo aby skutečně mlčel. 2. Cron job VŽDY jede přes agenta (`agent.process_direct`) — neagentní typ jobu neexistuje. Agent komentuje své mlčení místo aby skutečně mlčel.
3. Doručení hlídá druhý LLM `evaluate_response` (`nanobot/utils/evaluator.py`), který je **fail-open**: při jakékoli chybě nebo když model nevrátí tool-call → `return True` (doruč). V journalu ~20×/den `"no tool call returned, defaulting to notify"`. 3. Doručení hlídá druhý LLM `evaluate_response` (`nanobot/utils/evaluator.py`), který je **fail-open**: při jakékoli chybě nebo když model nevrátí tool-call → `return True` (doruč). V journalu ~20×/den `"no tool call returned, defaulting to notify"`.
4. → Únik nastane jen v minutách, kdy evaluator LLM selže. Nedeterministické a model-závislé (zhoršilo se po přepnutí na slabší Ollama-cloud modely) → "jen sem tam". Text je pokaždé čerstvě generovaný → "pokaždé jiná zpráva". 4. → Únik nastane jen v minutách, kdy evaluator LLM selže. Nedeterministické a model-závislé (zhoršilo se po přepnutí na slabší Ollama-cloud modely) → "jen sem tam". Text je pokaždé čerstvě generovaný → "pokaždé jiná zpráva".
**Co fungovalo (řešení — Design A, mimo agenta):** **Co fungovalo (řešení — Design A, mimo agenta):**
- Nový `skills/remind/scripts/remind_send.py`: čte reminder.yaml, při shodě posílá **přímo přes Telegram Bot API** (token z `~/.nanobot/config.json``channels.telegram.token`), deduplikuje přes `.reminder_state.json` (každý fire 1×), loguje do `reminder.log` (`ISO-čas \t text`). Žádný LLM, žádný nanobot proces. Stdlib `urllib` (deps jen croniter+pyyaml). - Nový `skills/remind/scripts/remind_send.py`: čte reminder.yaml, při shodě posílá **přímo přes Telegram Bot API** (token z `~/.nanobot/config.json``channels.telegram.token`), deduplikuje přes `.reminder_state.json` (každý fire 1×), loguje do `reminder.log` (`ISO-čas \t text`). Žádný LLM, žádný nanobot proces. Stdlib `urllib` (deps jen croniter+pyyaml).
- Odebrán nanobot cron job `remind-check` (id 1a7bb634): stop service → filtr v jobs.json → start. Po restartu se nevrátil. Záloha `cron/jobs.json.bak-*`. - Odebrán nanobot cron job `remind-check` (id 1a7bb634): stop service → filtr v jobs.json → start. Po restartu se nevrátil. Záloha `cron/jobs.json.bak-*`.
- Crontab pod uživatelem nanobot (`crontab -l`): `* * * * * uv run …/remind_send.py >> reminder_cron.log 2>&1`, s `HOME` + `PATH` v hlavičce (cron má holé prostředí). Cron daemon `cron.service` na hostu běží. Ověřeno: `(nanobot) CMD (uv run …remind_send.py)` v journalu každou minutu, log prázdný (běží čistě). - Crontab pod uživatelem nanobot (`crontab -l`): `* * * * * uv run …/remind_send.py >> reminder_cron.log 2>&1`, s `HOME` + `PATH` v hlavičce (cron má holé prostředí). Cron daemon `cron.service` na hostu běží. Ověřeno: `(nanobot) CMD (uv run …remind_send.py)` v journalu každou minutu, log prázdný (běží čistě).
@@ -392,6 +532,7 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
**Cíl:** Zastavit opakované Telegram zprávy "Žádné připomínky k doručení." chodicí každou minutu. **Cíl:** Zastavit opakované Telegram zprávy "Žádné připomínky k doručení." chodicí každou minutu.
**Co jsem zkusil:** **Co jsem zkusil:**
1. Zkontroloval hooks v `.claude/settings.json` a `~/.claude/settings.json` — nenašel nic relevantního. 1. Zkontroloval hooks v `.claude/settings.json` a `~/.claude/settings.json` — nenašel nic relevantního.
2. `CronList` v harness — žádné naplánované joby tady. 2. `CronList` v harness — žádné naplánované joby tady.
3. Hledal skripty s "telegram"/"remind" na serveru — našel `/home/nanobot/.nanobot/workspace/cron/jobs.json`. 3. Hledal skripty s "telegram"/"remind" na serveru — našel `/home/nanobot/.nanobot/workspace/cron/jobs.json`.
@@ -400,10 +541,12 @@ Zdroj: `/home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packa
6. Problém: prompt agenta říkal "If output is non-empty, forward" — LLM to ignoroval a posílal "Žádné připomínky" jako potvrzení. 6. Problém: prompt agenta říkal "If output is non-empty, forward" — LLM to ignoroval a posílal "Žádné připomínky" jako potvrzení.
**Co fungovalo a proč:** **Co fungovalo a proč:**
- Upravil `payload.message` v `jobs.json` přes Python in-place editor na serveru — přidal explicitní instrukci "If the output is EMPTY, do NOT send any message — exit silently." - Upravil `payload.message` v `jobs.json` přes Python in-place editor na serveru — přidal explicitní instrukci "If the output is EMPTY, do NOT send any message — exit silently."
- Restart nanobot služby: `su - nanobot -s /bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u nanobot) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u nanobot)/bus systemctl --user restart nanobot.service'` - Restart nanobot služby: `su - nanobot -s /bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u nanobot) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u nanobot)/bus systemctl --user restart nanobot.service'`
**Slepá ulička:** **Slepá ulička:**
- `kill -HUP <pid>` na gateway proces — nanobot se nerestartoval, ale **skončil** a nenaskočil zpět. Poté bylo třeba ho ručně nastartovat výše uvedeným příkazem. - `kill -HUP <pid>` na gateway proces — nanobot se nerestartoval, ale **skončil** a nenaskočil zpět. Poté bylo třeba ho ručně nastartovat výše uvedeným příkazem.
- `su - nanobot -c 'systemctl --user restart ...'` bez `XDG_RUNTIME_DIR` a `DBUS_SESSION_BUS_ADDRESS` selhalo s "bus not defined". - `su - nanobot -c 'systemctl --user restart ...'` bez `XDG_RUNTIME_DIR` a `DBUS_SESSION_BUS_ADDRESS` selhalo s "bus not defined".
@@ -432,6 +575,7 @@ Změny v `jobs.json` z předchozí session se **neuložily** — soubor stále o
**Co jsem zkusil / Co fungovalo:** **Co jsem zkusil / Co fungovalo:**
Průzkum ukázal dva Python skripty se odlišnou architekturou: Průzkum ukázal dva Python skripty se odlišnou architekturou:
- `remind_runner.py` byl aktivní (cron job ho spouštěl), ale špatný — posílal Telegram přímo přes API (obcházel nanobot), neměl `at` field support, měl hardcoded UTC+2 - `remind_runner.py` byl aktivní (cron job ho spouštěl), ale špatný — posílal Telegram přímo přes API (obcházel nanobot), neměl `at` field support, měl hardcoded UTC+2
- `remind_check.py` měl správnou architekturu (stdout → nanobot → user), podporoval `at` field, ale měl UTC timezone místo Prague - `remind_check.py` měl správnou architekturu (stdout → nanobot → user), podporoval `at` field, ale měl UTC timezone místo Prague
@@ -453,6 +597,7 @@ Průzkum ukázal dva Python skripty se odlišnou architekturou:
**Cíl:** Zjednodušit a zpřehlednit CLAUDE.md pro nanobot projekt + vytvořit čistou šablonu pro reuse v dalších projektech. **Cíl:** Zjednodušit a zpřehlednit CLAUDE.md pro nanobot projekt + vytvořit čistou šablonu pro reuse v dalších projektech.
**Změny (první vlna):** **Změny (první vlna):**
- CLAUDE.md zkráceno z ~84 na ~46 řádků — pravidla přepsána do kompaktní tabulky + odrážkových sekcí - CLAUDE.md zkráceno z ~84 na ~46 řádků — pravidla přepsána do kompaktní tabulky + odrážkových sekcí
- Přidán `plans.md` do tabulky tracking souborů (byl osiřelý, v CLAUDE.md nezmiňovaný) - Přidán `plans.md` do tabulky tracking souborů (byl osiřelý, v CLAUDE.md nezmiňovaný)
- Sekce commitu zhuštěna na 4 odrážky (byl zbytečně rozvláčný) - Sekce commitu zhuštěna na 4 odrážky (byl zbytečně rozvláčný)
@@ -460,6 +605,7 @@ Průzkum ukázal dva Python skripty se odlišnou architekturou:
- Záložní šablona uložena do `/home/lachtan/CLAUDE.template.md` - Záložní šablona uložena do `/home/lachtan/CLAUDE.template.md`
**Změny (druhá vlna — workflow zkoušení/ladění):** **Změny (druhá vlna — workflow zkoušení/ladění):**
- **Distilace history → knowledge:** vyřešené netriviální chyby se z deníku vytahují jako jednořádkové *problém → příčina → fix* do `knowledge.md` s odkazem zpět. Automaticky, hned po vyřešení. - **Distilace history → knowledge:** vyřešené netriviální chyby se z deníku vytahují jako jednořádkové *problém → příčina → fix* do `knowledge.md` s odkazem zpět. Automaticky, hned po vyřešení.
- **Otevřené otázky** mají domov v `todo.md` (prefix `otázka:`); po uzavření odpověď → `knowledge.md`. - **Otevřené otázky** mají domov v `todo.md` (prefix `otázka:`); po uzavření odpověď → `knowledge.md`.
- **Zamítnuté možnosti** se zaznamenávají do `knowledge.md` (co a proč zavrženo, ať se netestuje znovu). - **Zamítnuté možnosti** se zaznamenávají do `knowledge.md` (co a proč zavrženo, ať se netestuje znovu).
@@ -780,9 +926,11 @@ Gateway restartován, `nanobot.service` aktivní s opraveným configem.
1. Ověřeno, že `glm-5.1:cloud` je v `curl http://nvidia.hell:11434/api/tags` (figurovalo už ve výpisu cloud modelů z [záznamu o Ollama provideru](#2026-05-26--ollama-na-nvidiahell-jako-druhý-provider--přejmenování-presetů)). 1. Ověřeno, že `glm-5.1:cloud` je v `curl http://nvidia.hell:11434/api/tags` (figurovalo už ve výpisu cloud modelů z [záznamu o Ollama provideru](#2026-05-26--ollama-na-nvidiahell-jako-druhý-provider--přejmenování-presetů)).
2. `~/.nanobot/config.json` rozšířen přes Python in-place editor o nový preset: 2. `~/.nanobot/config.json` rozšířen přes Python in-place editor o nový preset:
```json ```json
"glm-5.1-ollama": { "provider": "ollama", "model": "glm-5.1:cloud" } "glm-5.1-ollama": { "provider": "ollama", "model": "glm-5.1:cloud" }
``` ```
3. **Restart služby zablokován** auto-mode klasifikátorem (`systemctl --user restart nanobot.service` přes SSH). Uživatel musí restart spustit ručně, nebo povolit Bash pravidlo. 3. **Restart služby zablokován** auto-mode klasifikátorem (`systemctl --user restart nanobot.service` přes SSH). Uživatel musí restart spustit ručně, nebo povolit Bash pravidlo.
**Otevřené:** restart `nanobot.service`, ověřit `/model glm-5.1-ollama` v chatu. **Otevřené:** restart `nanobot.service`, ověřit `/model glm-5.1-ollama` v chatu.
@@ -796,6 +944,7 @@ Gateway restartován, `nanobot.service` aktivní s opraveným configem.
**Zjištění (zdroj: DeepWiki CLI reference + `nanobot gateway --help`):** nanobot defaultně `logger.disable("nanobot")`. Gateway má `-v`/`--verbose` (DEBUG), agent má `--logs`. WebUI běží uvnitř gateway (websocket channel, port 8765) → `-v` ho pokrývá, separátní přepínač není. **Zjištění (zdroj: DeepWiki CLI reference + `nanobot gateway --help`):** nanobot defaultně `logger.disable("nanobot")`. Gateway má `-v`/`--verbose` (DEBUG), agent má `--logs`. WebUI běží uvnitř gateway (websocket channel, port 8765) → `-v` ho pokrývá, separátní přepínač není.
**Provedeno:** **Provedeno:**
1. `sed` úprava `~/.config/systemd/user/nanobot.service`: `ExecStart=… nanobot gateway` → `… gateway -v`. 1. `sed` úprava `~/.config/systemd/user/nanobot.service`: `ExecStart=… nanobot gateway` → `… gateway -v`.
2. `systemctl --user daemon-reload && restart` → `active`. 2. `systemctl --user daemon-reload && restart` → `active`.
3. Test: přes WebUI poslána zpráva „pouzij python pro zjisteni aktualniho casu“. 3. Test: přes WebUI poslána zpráva „pouzij python pro zjisteni aktualniho casu“.
@@ -811,11 +960,14 @@ Gateway restartován, `nanobot.service` aktivní s opraveným configem.
**Cíl:** zpřístupnit `deepseek-v3.2:cloud` z Ollama (přes `nvidia.hell`) jako přepínatelný model — kandidát na levný autonomní workhorse (cron/heartbeat). Vychází z rozvahy „top 10 modelů pro nanobot" (Ollama cloud + OpenRouter, bez Anthropic/OpenAI). **Cíl:** zpřístupnit `deepseek-v3.2:cloud` z Ollama (přes `nvidia.hell`) jako přepínatelný model — kandidát na levný autonomní workhorse (cron/heartbeat). Vychází z rozvahy „top 10 modelů pro nanobot" (Ollama cloud + OpenRouter, bez Anthropic/OpenAI).
**Provedeno (postup dle [knowledge.md](knowledge.md) „Postup: přidání nového modelu"):** **Provedeno (postup dle [knowledge.md](knowledge.md) „Postup: přidání nového modelu"):**
1. Ověřeno `curl http://nvidia.hell:11434/api/tags` → `deepseek-v3.2:cloud` přítomno. 1. Ověřeno `curl http://nvidia.hell:11434/api/tags` → `deepseek-v3.2:cloud` přítomno.
2. `~/.nanobot/config.json` rozšířen přes Python in-place editor: 2. `~/.nanobot/config.json` rozšířen přes Python in-place editor:
```json ```json
"deepseek-v3.2-ollama": { "provider": "ollama", "model": "deepseek-v3.2:cloud" } "deepseek-v3.2-ollama": { "provider": "ollama", "model": "deepseek-v3.2:cloud" }
``` ```
3. `systemctl --user restart nanobot.service` → `active`. 3. `systemctl --user restart nanobot.service` → `active`.
**Výsledek:** preset funkční, v chatu přepnutelný `/model deepseek-v3.2-ollama`. Rutinní změna, žádné nové zjištění do knowledge.md. **Výsledek:** preset funkční, v chatu přepnutelný `/model deepseek-v3.2-ollama`. Rutinní změna, žádné nové zjištění do knowledge.md.
@@ -849,6 +1001,7 @@ Gateway restartován, `nanobot.service` aktivní s opraveným configem.
6. **Chat_id discovery** (vyřešeno bez patche). ContextBuilder vkládá do runtime contextu zprávy `Channel: <name>` a `Chat ID: <id>` (`context.py:123-139`). Skill v SKILL.md instruuje agenta, ať si chat_id přečte odtud, takže žádný session_info() helper tool nepotřebujeme. 6. **Chat_id discovery** (vyřešeno bez patche). ContextBuilder vkládá do runtime contextu zprávy `Channel: <name>` a `Chat ID: <id>` (`context.py:123-139`). Skill v SKILL.md instruuje agenta, ať si chat_id přečte odtud, takže žádný session_info() helper tool nepotřebujeme.
**Co fungovalo a proč:** Smoke test prošel za 3 sekundy end-to-end: **Co fungovalo a proč:** Smoke test prošel za 3 sekundy end-to-end:
- Vytvořen fake task soubor `2026-05-28T172259-smoke-test.md` v `tasks/tmp/`, `mv` do `tasks/inbox/`. - Vytvořen fake task soubor `2026-05-28T172259-smoke-test.md` v `tasks/tmp/`, `mv` do `tasks/inbox/`.
- Systemd `.path` unit spustil `tasks-daemon.service` okamžitě (z `inactive` do `active`). - Systemd `.path` unit spustil `tasks-daemon.service` okamžitě (z `inactive` do `active`).
- Daemon přesunul `inbox/ → running/`, zavolal `Nanobot.run("Odpověz jedním slovem: kolik je 2+2?")`, dostal `"čtyři"`. - Daemon přesunul `inbox/ → running/`, zavolal `Nanobot.run("Odpověz jedním slovem: kolik je 2+2?")`, dostal `"čtyři"`.
@@ -856,6 +1009,7 @@ Gateway restartován, `nanobot.service` aktivní s opraveným configem.
- `telegram_send(chat_id, "✅ Hotovo: čtyři. Detail: workspace/tasks/done/…")` — `NOTIFIED 8826147089` v daemon logu. - `telegram_send(chat_id, "✅ Hotovo: čtyři. Detail: workspace/tasks/done/…")` — `NOTIFIED 8826147089` v daemon logu.
Logy: Logy:
``` ```
2026-05-28T17:23:00 DRAIN start 1 task(s) 2026-05-28T17:23:00 DRAIN start 1 task(s)
2026-05-28T17:23:00 START 2026-05-28T172259-smoke-test.md 2026-05-28T17:23:00 START 2026-05-28T172259-smoke-test.md
@@ -884,11 +1038,13 @@ Logy:
``` ```
**Soubory:** **Soubory:**
- `~/.nanobot/workspace/skills/detach/SKILL.md` + `scripts/tasks-daemon.py` + `systemd/tasks-daemon.{path,service}` (server) - `~/.nanobot/workspace/skills/detach/SKILL.md` + `scripts/tasks-daemon.py` + `systemd/tasks-daemon.{path,service}` (server)
- Tracking: [skills/detach/](skills/detach/) v tomto repu - Tracking: [skills/detach/](skills/detach/) v tomto repu
- Plán: [/home/lachtan/.claude/plans/pojdme-jeste-chvili-planovat-binary-harp.md](/home/lachtan/.claude/plans/pojdme-jeste-chvili-planovat-binary-harp.md) (kompletní rozhodovací stopa) - Plán: [/home/lachtan/.claude/plans/pojdme-jeste-chvili-planovat-binary-harp.md](/home/lachtan/.claude/plans/pojdme-jeste-chvili-planovat-binary-harp.md) (kompletní rozhodovací stopa)
**Co zbývá:** **Co zbývá:**
- Reálný test skillu z chatu (uživatel triggerne přes Telegram, ověří, že agent správně přečte Chat ID z runtime contextu, vyrobí slug+timestamp, atomicky přesune do inbox/). - Reálný test skillu z chatu (uživatel triggerne přes Telegram, ověří, že agent správně přečte Chat ID z runtime contextu, vyrobí slug+timestamp, atomicky přesune do inbox/).
- Smoke test #2 — timeout (úkol >15min → status=failed). - Smoke test #2 — timeout (úkol >15min → status=failed).
- Smoke test #3 — primární use case (deep research s `subagent`/`web_fetch`). - Smoke test #3 — primární use case (deep research s `subagent`/`web_fetch`).
@@ -900,6 +1056,7 @@ Logy:
## 2026-05-28 18:37 — Detach skill iterace #2: fallback chat_id + subactions list/read + SKILL.md do EN ## 2026-05-28 18:37 — Detach skill iterace #2: fallback chat_id + subactions list/read + SKILL.md do EN
**Cíl:** opravit dva nedostatky z reálného testu (uživatel pustil úkol z WebUI): **Cíl:** opravit dva nedostatky z reálného testu (uživatel pustil úkol z WebUI):
1. **Bug 1**: Telegram notifikace selhala HTTP 400, protože WebSocket session má v runtime contextu `Chat ID: <UUID>`, ne Telegram chat ID. Skill ho poslušně dal do frontmatteru, daemon ho strčil do Bot API. 1. **Bug 1**: Telegram notifikace selhala HTTP 400, protože WebSocket session má v runtime contextu `Chat ID: <UUID>`, ne Telegram chat ID. Skill ho poslušně dal do frontmatteru, daemon ho strčil do Bot API.
2. **Bug 2**: i kdyby notifikace dorazila, je v ní jen jednořádkové shrnutí + cesta k souboru — chyběl ergonomický mechanismus pro přečtení výsledku v chatu. 2. **Bug 2**: i kdyby notifikace dorazila, je v ní jen jednořádkové shrnutí + cesta k souboru — chyběl ergonomický mechanismus pro přečtení výsledku v chatu.
@@ -910,12 +1067,15 @@ Logy:
1. **Daemon `resolve_telegram_chat_id(fm)`:** vrátí `(chat_id, source)`. Když `fm.channel == "telegram"` → použij `fm.chat_id` (multi-user ready). Jinak → načti `fallback_telegram_chat_id` z `~/.nanobot/workspace/skills/detach/config.json`. Log line `NOTIFY chat=<id> source=<frontmatter|fallback>`. 1. **Daemon `resolve_telegram_chat_id(fm)`:** vrátí `(chat_id, source)`. Když `fm.channel == "telegram"` → použij `fm.chat_id` (multi-user ready). Jinak → načti `fallback_telegram_chat_id` z `~/.nanobot/workspace/skills/detach/config.json`. Log line `NOTIFY chat=<id> source=<frontmatter|fallback>`.
2. **Nový soubor `config.json` ve skill adresáři:** 2. **Nový soubor `config.json` ve skill adresáři:**
```json ```json
{"fallback_telegram_chat_id": "8826147089"} {"fallback_telegram_chat_id": "8826147089"}
``` ```
Editovatelný bez touch daemon kódu, single-user setup vyhraje hardcode. Editovatelný bez touch daemon kódu, single-user setup vyhraje hardcode.
3. **Telegram notifikace text** přepracován — obsahuje slug v první řádce + výzvu „v chatu si vyžádej plný report: `výsledek <slug>`": 3. **Telegram notifikace text** přepracován — obsahuje slug v první řádce + výzvu „v chatu si vyžádej plný report: `výsledek <slug>`":
``` ```
✅ Hotovo: `fallback-test` ✅ Hotovo: `fallback-test`
@@ -943,6 +1103,7 @@ Logy:
Fake task s `channel: websocket`, `chat_id: "68254619-fake-websocket-uuid-test"` → daemon rozeznal non-telegram channel → použil fallback `8826147089` → HTTP 200 OK. Trvání 6s (krátký úkol bez tools). Fake task s `channel: websocket`, `chat_id: "68254619-fake-websocket-uuid-test"` → daemon rozeznal non-telegram channel → použil fallback `8826147089` → HTTP 200 OK. Trvání 6s (krátký úkol bez tools).
**Co zbývá:** **Co zbývá:**
- **Reálný retest Bug 1**: uživatel spustí detach z WebUI → Telegram zpráva musí dorazit. - **Reálný retest Bug 1**: uživatel spustí detach z WebUI → Telegram zpráva musí dorazit.
- **Reálný retest Bug 2**: v chatu (Telegram nebo WebUI) řekni „výsledek fallback-test" → agent musí přečíst soubor a předložit Result sekci. Také „výsledky?" → list všech. - **Reálný retest Bug 2**: v chatu (Telegram nebo WebUI) řekni „výsledek fallback-test" → agent musí přečíst soubor a předložit Result sekci. Také „výsledky?" → list všech.
- **Fáze 2b odložena** (Fáze 3): WebSocket push z daemonu do nanobot gateway pro live notifikaci v aktivní WebUI session. Důvod: nanobot WS gateway vyžaduje JWT auth (`channels.websocket.token_issue_secret`), WS klient v daemonu, envelope format — komplexní, zatím vyřešeno přes `výsledek <slug>` pull mechanismus. - **Fáze 2b odložena** (Fáze 3): WebSocket push z daemonu do nanobot gateway pro live notifikaci v aktivní WebUI session. Důvod: nanobot WS gateway vyžaduje JWT auth (`channels.websocket.token_issue_secret`), WS klient v daemonu, envelope format — komplexní, zatím vyřešeno přes `výsledek <slug>` pull mechanismus.
@@ -962,6 +1123,7 @@ Fake task s `channel: websocket`, `chat_id: "68254619-fake-websocket-uuid-test"`
4. **Ověření na serveru:** `grep -A4 'MEMORY.md'` vrátil nový krok 4 a nový Rules řádek — obojí správně. 4. **Ověření na serveru:** `grep -A4 'MEMORY.md'` vrátil nový krok 4 a nový Rules řádek — obojí správně.
**Co zbývá (pro uživatele):** **Co zbývá (pro uživatele):**
- End-to-end test: vybrat fakt, který již je v `workspace/memory/MEMORY.md`, poslat ho agentem jako `keep: <fakt>` → agent by měl reagovat hláškou „Already in MEMORY.md: …" a defaultně nepsat. - End-to-end test: vybrat fakt, který již je v `workspace/memory/MEMORY.md`, poslat ho agentem jako `keep: <fakt>` → agent by měl reagovat hláškou „Already in MEMORY.md: …" a defaultně nepsat.
- Regrese dedup v `keep.md` pořád funguje: nový fakt 2× po sobě → při druhém pokus se agent zeptá na duplicitu v `keep.md`. - Regrese dedup v `keep.md` pořád funguje: nový fakt 2× po sobě → při druhém pokus se agent zeptá na duplicitu v `keep.md`.
@@ -980,11 +1142,13 @@ Fake task s `channel: websocket`, `chat_id: "68254619-fake-websocket-uuid-test"`
3. **USER.md reference** — append sekce `## workspace/keep.md` + popis na konec `/home/nanobot/.nanobot/workspace/USER.md` (jako `nanobot` user přes SSH, tee -a). Ověřeno `tail -7`. 3. **USER.md reference** — append sekce `## workspace/keep.md` + popis na konec `/home/nanobot/.nanobot/workspace/USER.md` (jako `nanobot` user přes SSH, tee -a). Ověřeno `tail -7`.
**Ověření (sanity check):** **Ověření (sanity check):**
- `ls -la /home/nanobot/.nanobot/workspace/skills/keep/` → `SKILL.md` owned `nanobot:nanobot`. - `ls -la /home/nanobot/.nanobot/workspace/skills/keep/` → `SKILL.md` owned `nanobot:nanobot`.
- `head -25 SKILL.md` → frontmatter + tělo správně. - `head -25 SKILL.md` → frontmatter + tělo správně.
- `tail -7 USER.md` → sekce `## workspace/keep.md` na konci. - `tail -7 USER.md` → sekce `## workspace/keep.md` na konci.
**Co zbývá (pro uživatele):** **Co zbývá (pro uživatele):**
- End-to-end test: v Telegramu/WebUI říct „keep: Honza z marketingu má alergii na arašídy" → ověřit `keep.md` na serveru. - End-to-end test: v Telegramu/WebUI říct „keep: Honza z marketingu má alergii na arašídy" → ověřit `keep.md` na serveru.
- Persistent awareness test: v nové session se zeptat na Honzu → agent zmíní alergii. - Persistent awareness test: v nové session se zeptat na Honzu → agent zmíní alergii.
- Compaction test: naplnit `keep.md` na 151+ řádků, spustit `/keep`, ověřit compaction report. - Compaction test: naplnit `keep.md` na 151+ řádků, spustit `/keep`, ověřit compaction report.
@@ -1003,13 +1167,14 @@ Fake task s `channel: websocket`, `chat_id: "68254619-fake-websocket-uuid-test"`
4. **`str()` wrappy pryč** — `shutil.move()` přijímá path-like objekty od Python 3.9, `str()` byl cargo-cult. Odstraněno ze 4 výskytů (3× v `process_task()`, 1× zbyl jen argument `running → target_dir`). 4. **`str()` wrappy pryč** — `shutil.move()` přijímá path-like objekty od Python 3.9, `str()` byl cargo-cult. Odstraněno ze 4 výskytů (3× v `process_task()`, 1× zbyl jen argument `running → target_dir`).
5. **Docstring sjednocen na CZ** — EN anotace v hlavičce přeložena do češtiny, diff se tím nezvětšil nad limit. 5. **Docstring sjednocen na CZ** — EN anotace v hlavičce přeložena do češtiny, diff se tím nezvětšil nad limit.
6. **tasks-daemon.service doplněn o `Environment=PATH=%h/.local/bin:/usr/bin:/bin`** — bez toho by systemd nenašel `uv` (není v defaultní PATH user unitů). `daemon-reload` přes `XDG_RUNTIME_DIR=/run/user/1000`. 6. **tasks-daemon.service doplněn o `Environment=PATH=%h/.local/bin:/usr/bin:/bin`** — bez toho by systemd nenašel `uv` (není v defaultní PATH user unitů). `daemon-reload` přes `XDG_RUNTIME_DIR=/run/user/1000`.
7. **Smoke test prošel**: task `2026-05-28T174616-uv-smoke.md` (`channel=telegram`, `chat_id=8826147089`, goal=`odpověz \`hotovo\``) → daemon zpracoval za 4s, log: `START`, `NOTIFY chat=8826147089 source=frontmatter`, `END status=done duration=4s`. Telegram zpráva dorazila. 7. **Smoke test prošel**: task `2026-05-28T174616-uv-smoke.md` (`channel=telegram`, `chat_id=8826147089`, goal=`odpověz \`hotovo\``) → daemon zpracoval za 4s, log:`START`,`NOTIFY chat=8826147089 source=frontmatter`,`END status=done duration=4s`. Telegram zpráva dorazila.
**Diff:** 23 changed lines v `tasks-daemon.py` (14 insertions, 9 deletions) — pod limitem 30. **Diff:** 23 changed lines v `tasks-daemon.py` (14 insertions, 9 deletions) — pod limitem 30.
**Rsync:** `rsync` přes `nanobot` uživatele selhal (`rsync: command not found` na straně serveru), použit `scp` přes `root` + `chown nanobot:nanobot`. **Rsync:** `rsync` přes `nanobot` uživatele selhal (`rsync: command not found` na straně serveru), použit `scp` přes `root` + `chown nanobot:nanobot`.
**Co zbývá:** **Co zbývá:**
- Verifikace z plánu: `uv tool uninstall nanobot-ai && uv tool install nanobot-ai` přežití (důkaz nezávislosti na tool venv cestě) — nízká priorita, PEP 723 pattern to garantuje designově. - Verifikace z plánu: `uv tool uninstall nanobot-ai && uv tool install nanobot-ai` přežití (důkaz nezávislosti na tool venv cestě) — nízká priorita, PEP 723 pattern to garantuje designově.
--- ---
@@ -1276,6 +1441,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Uživatel zaznamenal, že Claude memory.md vzor ukládá u feedback/project paměti i *why* (`**Why:**` / `**How to apply:**`), a ptal se, zda by `/keep` neměl dělat totéž. Cílem bylo minimalisticky upravit prompt skillu, aby why ukládal — ale jen kde to dává smysl. **Cíl:** Uživatel zaznamenal, že Claude memory.md vzor ukládá u feedback/project paměti i *why* (`**Why:**` / `**How to apply:**`), a ptal se, zda by `/keep` neměl dělat totéž. Cílem bylo minimalisticky upravit prompt skillu, aby why ukládal — ale jen kde to dává smysl.
**Co jsem zkusil / co fungovalo a proč:** **Co jsem zkusil / co fungovalo a proč:**
- Analýza: Claude vzor přidává why **jen podle typu** — feedback/project ano, reference/fakt ne. `/keep` je ekvivalent reference/fakt vrstvy (terse, auto-load každý tah přes USER.md referenci), why-vrstvou nanobotu je Dream → MEMORY.md. Bloat why ke každému řádku by zdvojnásobil trvalý token cost. - Analýza: Claude vzor přidává why **jen podle typu** — feedback/project ano, reference/fakt ne. `/keep` je ekvivalent reference/fakt vrstvy (terse, auto-load každý tah přes USER.md referenci), why-vrstvou nanobotu je Dream → MEMORY.md. Bloat why ke každému řádku by zdvojnásobil trvalý token cost.
- Potvrzeno článkem Nick Babich „MEMORY.md for Claude Code projects" (UX Planet, 2026-05-28): bod #3 „Explain WHY, Not Just WHAT" + #5 „Record Failures and Dead Ends" — ale jeho příklad why (`Switched to Redis because…`) je **rozhodnutí**, ne fakt → potvrzuje úzkou variantu. - Potvrzeno článkem Nick Babich „MEMORY.md for Claude Code projects" (UX Planet, 2026-05-28): bod #3 „Explain WHY, Not Just WHAT" + #5 „Record Failures and Dead Ends" — ale jeho příklad why (`Switched to Redis because…`) je **rozhodnutí**, ne fakt → potvrzuje úzkou variantu.
- Iterace promptu (3 kola): (1) přidán why-řádek do kroku 2 + mantinel do Rules, s 2 few-shot příklady (decision, dead-end). (2) Po dotazu na model: lokální modely zavrženy, reálně běží Ollama Cloud (kimi-k2.6, deepseek-v3.2, qwen, glm-5.1) + OpenRouter (sonnet/haiku/gemini) — silné modely → fakt-vs-rozhodnutí zvládnou zero-shot → **oba příklady odebrány**. (3) Přidáno: pokud je vstup rozhodnutí/dead-end **bez důvodu**, model se **jednou doptá** na why (decline/self-evident → ulož bez něj). - Iterace promptu (3 kola): (1) přidán why-řádek do kroku 2 + mantinel do Rules, s 2 few-shot příklady (decision, dead-end). (2) Po dotazu na model: lokální modely zavrženy, reálně běží Ollama Cloud (kimi-k2.6, deepseek-v3.2, qwen, glm-5.1) + OpenRouter (sonnet/haiku/gemini) — silné modely → fakt-vs-rozhodnutí zvládnou zero-shot → **oba příklady odebrány**. (3) Přidáno: pokud je vstup rozhodnutí/dead-end **bez důvodu**, model se **jednou doptá** na why (decline/self-evident → ulož bez něj).
@@ -1290,6 +1456,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Dát nanobotovi pravidla pro práci s Pythonem — uv tool, type hints, linter/formatter, PEP + clean code. **Cíl:** Dát nanobotovi pravidla pro práci s Pythonem — uv tool, type hints, linter/formatter, PEP + clean code.
**Co jsem zkusil / udělal:** **Co jsem zkusil / udělal:**
- Založen `skills/python/SKILL.md`. Tělo vychází z `.claude/rules/python.md` (použito celé) + doplněna sekce **Tooling** (uv, `ruff format`, `ruff check --fix`, definice „done"). - Založen `skills/python/SKILL.md`. Tělo vychází z `.claude/rules/python.md` (použito celé) + doplněna sekce **Tooling** (uv, `ruff format`, `ruff check --fix`, definice „done").
- Z `.claude/rules/clean-code.md` přidány jen body zvyšující čitelnost/strukturu: boolean flag args, CQS, param count 03, fail fast / guard clauses, max zanoření, magic numbers → konstanty. Designové body (composition, DI, immutability) vynechány. - Z `.claude/rules/clean-code.md` přidány jen body zvyšující čitelnost/strukturu: boolean flag args, CQS, param count 03, fail fast / guard clauses, max zanoření, magic numbers → konstanty. Designové body (composition, DI, immutability) vynechány.
- Na žádost uživatele vyházeny věci, které modely dělají samy: casing (snake_case/PascalCase), 4-mezery, f-stringy, řazení importů. Naming + Imports sekce zrušeny, Style zúžen na line-length 120. - Na žádost uživatele vyházeny věci, které modely dělají samy: casing (snake_case/PascalCase), 4-mezery, f-stringy, řazení importů. Naming + Imports sekce zrušeny, Style zúžen na line-length 120.
@@ -1304,6 +1471,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Stejně jako `python` — převést `.claude/rules/bash.md` na nanobotí skill. **Cíl:** Stejně jako `python` — převést `.claude/rules/bash.md` na nanobotí skill.
**Co jsem udělal:** **Co jsem udělal:**
- Založen `skills/bash/SKILL.md` z `.claude/rules/bash.md`. - Založen `skills/bash/SKILL.md` z `.claude/rules/bash.md`.
- Vyházeno: Naming sekce (casing — modely trefují samy), `[[ ]]` vs `[ ]` (default), zmínka o `check-bash.sh` hooku (hook tohoto repa, na serveru irelevantní), `paths:` frontmatter (Claude Code feature). - Vyházeno: Naming sekce (casing — modely trefují samy), `[[ ]]` vs `[ ]` (default), zmínka o `check-bash.sh` hooku (hook tohoto repa, na serveru irelevantní), `paths:` frontmatter (Claude Code feature).
- Ponecháno to, kde modely v bashi reálně chybují / projektový standard: strict mode, double-quoting, local/readonly + návrat přes stdout, arrays vs IFS, `command -v` místo `which`, stderr, mktemp+trap, script_dir idiom, shellcheck, hook exit kódy 0/2. - Ponecháno to, kde modely v bashi reálně chybují / projektový standard: strict mode, double-quoting, local/readonly + návrat přes stdout, arrays vs IFS, `command -v` místo `which`, stderr, mktemp+trap, script_dir idiom, shellcheck, hook exit kódy 0/2.
@@ -1318,6 +1486,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Tvrdá data na dotaz „je minimax línej?" — porovnat time-to-first-token, rychlost generování a celkovou dobu odpovědi mezi `glm-5.1:cloud` a `minimax-m3:cloud` na reálné úloze typu `/remind list`. **Cíl:** Tvrdá data na dotaz „je minimax línej?" — porovnat time-to-first-token, rychlost generování a celkovou dobu odpovědi mezi `glm-5.1:cloud` a `minimax-m3:cloud` na reálné úloze typu `/remind list`.
**Co jsem zkusil:** **Co jsem zkusil:**
1. **Journal nestačí.** `journalctl --user -u nanobot` loguje jen `Processing message → Tool call → Response` (časové značky). **Žádné počty tokenů, žádné tok/s, žádný TTFT.** Wall-clock z journalu navíc míchá běh nástrojů (`uv run` cold-start ~27 s) a víc kol tool-callů → na rychlost modelu nepoužitelné. (Z journalu jen kontext: na `minimax-m3` přepnuto poprvé 2026-06-05, dnes 09:03; jeden reálný `/remind list` pod m3 trval wall 37 s včetně 2 tool-callů.) 1. **Journal nestačí.** `journalctl --user -u nanobot` loguje jen `Processing message → Tool call → Response` (časové značky). **Žádné počty tokenů, žádné tok/s, žádný TTFT.** Wall-clock z journalu navíc míchá běh nástrojů (`uv run` cold-start ~27 s) a víc kol tool-callů → na rychlost modelu nepoužitelné. (Z journalu jen kontext: na `minimax-m3` přepnuto poprvé 2026-06-05, dnes 09:03; jeden reálný `/remind list` pod m3 trval wall 37 s včetně 2 tool-callů.)
2. **Ollama `:cloud` nativní timing fields jsou `None`** (`eval_duration`, `prompt_eval_duration`, `load_duration`) — výpočet jede přes Ollamin cloud, lokální server na `nvidia.hell` je nezná. Vrací jen `eval_count` (out tok) + `total_duration`. 2. **Ollama `:cloud` nativní timing fields jsou `None`** (`eval_duration`, `prompt_eval_duration`, `load_duration`) — výpočet jede přes Ollamin cloud, lokální server na `nvidia.hell` je nezná. Vrací jen `eval_count` (out tok) + `total_duration`.
3. **Řešení = streaming `/api/chat`.** Skript `/tmp/bench_ollama.py` (na serveru): TTFT = čas prvního content chunku, generace = `eval_count / (total TTFT)`, identický prompt (`/remind list` formátování 8 položek do češtiny), 3 běhy/model. 3. **Řešení = streaming `/api/chat`.** Skript `/tmp/bench_ollama.py` (na serveru): TTFT = čas prvního content chunku, generace = `eval_count / (total TTFT)`, identický prompt (`/remind list` formátování 8 položek do češtiny), 3 běhy/model.
@@ -1332,6 +1501,7 @@ Běh 5 modelů s gate=3:
\* `eval_count / (total TTFT)`. \* `eval_count / (total TTFT)`.
**Co fungovalo a proč:** **Co fungovalo a proč:**
- **TTFT srovnatelný (~67 s u obou)** — minimax neprohrává startem, oba mají podobný cloud cold start. - **TTFT srovnatelný (~67 s u obou)** — minimax neprohrává startem, oba mají podobný cloud cold start.
- **Celková doba: minimax ~50 % delší** (medián ~10,8 s vs ~7,5 s glm), a to **přestože generuje 34× MÉNĚ tokenů** (~430 vs ~1480). To je hlavní důkaz „lenosti" minimaxu. - **Celková doba: minimax ~50 % delší** (medián ~10,8 s vs ~7,5 s glm), a to **přestože generuje 34× MÉNĚ tokenů** (~430 vs ~1480). To je hlavní důkaz „lenosti" minimaxu.
- **Generace tok/s:** minimax **~95120 tok/s je čisté číslo** (streamuje token po tokenu). GLM hodnotu „1300 tok/s" NEbrat doslovně — je to **artefakt cloud bufferu** (glm flushne odpověď v dávce po TTFT). Robustní srovnatelná metrika je **end-to-end průtok = out/total: glm ~198 tok/s vs minimax ~40 tok/s → glm ~5×.** - **Generace tok/s:** minimax **~95120 tok/s je čisté číslo** (streamuje token po tokenu). GLM hodnotu „1300 tok/s" NEbrat doslovně — je to **artefakt cloud bufferu** (glm flushne odpověď v dávce po TTFT). Robustní srovnatelná metrika je **end-to-end průtok = out/total: glm ~198 tok/s vs minimax ~40 tok/s → glm ~5×.**
@@ -1349,6 +1519,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Dát nanobot agentovi na serveru znalostní bázi o modelech, ze které může těžit při dotazech typu „jaký model na kódování / na zpracování fotky" — fakta a naměřené hodnoty, žádná hotová doporučení (volba je na agentovi). **Cíl:** Dát nanobot agentovi na serveru znalostní bázi o modelech, ze které může těžit při dotazech typu „jaký model na kódování / na zpracování fotky" — fakta a naměřené hodnoty, žádná hotová doporučení (volba je na agentovi).
**Co jsem zkusil:** **Co jsem zkusil:**
- Vytáhl reálné presety ze serverového `config.json``model_presets` (7 presetů: glm-5.1 default, minimax-m3, kimi-k2.6 na ollama; sonnet, haiku na openrouter; gemini-flash na gemini; gemini-flash-lite na openrouter). Liší se od lokálního `models.md`, který řešil jen 3 Ollama Cloud modely. - Vytáhl reálné presety ze serverového `config.json``model_presets` (7 presetů: glm-5.1 default, minimax-m3, kimi-k2.6 na ollama; sonnet, haiku na openrouter; gemini-flash na gemini; gemini-flash-lite na openrouter). Liší se od lokálního `models.md`, který řešil jen 3 Ollama Cloud modely.
- Konsolidoval do `knowledge/models.md` (CZ, facts-only): tabulka presetů (provider, model id, kontext, multimodalita), přímé měření rychlosti na Ollama Cloud (glm ~198 tok/s vs m3 ~40 tok/s, m3 o ~50 % pomalejší), AA profil verbozity, rozdíly ve schopnostech (multimodál, HLE, kódování, dlouhý kontext), caveaty (vendor vs produkce, prompt caching jen openrouter/anthropic/bedrock, ollama kvóta 3 paralelní), kontextová okna, appendix s MiniLoop měřeními (jasně označeno jako jiný kontext, ne agent presety). - Konsolidoval do `knowledge/models.md` (CZ, facts-only): tabulka presetů (provider, model id, kontext, multimodalita), přímé měření rychlosti na Ollama Cloud (glm ~198 tok/s vs m3 ~40 tok/s, m3 o ~50 % pomalejší), AA profil verbozity, rozdíly ve schopnostech (multimodál, HLE, kódování, dlouhý kontext), caveaty (vendor vs produkce, prompt caching jen openrouter/anthropic/bedrock, ollama kvóta 3 paralelní), kontextová okna, appendix s MiniLoop měřeními (jasně označeno jako jiný kontext, ne agent presety).
- `knowledge/README.md` jako rozcestník (vzor podle `develop/README.md`). - `knowledge/README.md` jako rozcestník (vzor podle `develop/README.md`).
@@ -1370,6 +1541,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Umožnit, aby detached task běžel na explicitně zvoleném modelu (jiném než default) — background = latence nebolí, vyplatí se silnější model. Když se model neuvede → default (zpětně kompatibilní). **Cíl:** Umožnit, aby detached task běžel na explicitně zvoleném modelu (jiném než default) — background = latence nebolí, vyplatí se silnější model. Když se model neuvede → default (zpětně kompatibilní).
**Co jsem zkusil:** **Co jsem zkusil:**
- Ověřil v upstreamu (`tmp/nanobot-upstream`), že `Nanobot.run()` model override nepřijímá, ale `AgentLoop.set_model_preset(name)` existuje (přesně to dělá `/model <preset>` v chatu) a hodí `KeyError` na neznámý preset. - Ověřil v upstreamu (`tmp/nanobot-upstream`), že `Nanobot.run()` model override nepřijímá, ale `AgentLoop.set_model_preset(name)` existuje (přesně to dělá `/model <preset>` v chatu) a hodí `KeyError` na neznámý preset.
- Zamítl variantu „zdědit aktivní model na kanálu": `/model` přepnutí je in-memory, globální (ne per-kanál), neperzistuje a daemon (samostatný proces) ho nevidí; šlo by jen patchem upstream `_build_runtime_context` → fork balíčku. Příliš drahé. Rozhodnutí s uživatelem: **explicitně v požadavku, jinak default**; resolve názvu **fuzzy proti `config.json`**. - Zamítl variantu „zdědit aktivní model na kanálu": `/model` přepnutí je in-memory, globální (ne per-kanál), neperzistuje a daemon (samostatný proces) ho nevidí; šlo by jen patchem upstream `_build_runtime_context` → fork balíčku. Příliš drahé. Rozhodnutí s uživatelem: **explicitně v požadavku, jinak default**; resolve názvu **fuzzy proti `config.json`**.
- `tasks_common.py`: `load_preset_names()` (čte `modelPresets` i `model_presets` — server má na disku snake_case), `resolve_preset(token, names)` (exact CI → unikátní substring → `KeyError` not found / ambiguous), `build_task_content(..., model=…)` přidá frontmatter řádek `model:` jen když je zadán. `format_result` ukáže `model:` v meta řádku. - `tasks_common.py`: `load_preset_names()` (čte `modelPresets` i `model_presets` — server má na disku snake_case), `resolve_preset(token, names)` (exact CI → unikátní substring → `KeyError` not found / ambiguous), `build_task_content(..., model=…)` přidá frontmatter řádek `model:` jen když je zadán. `format_result` ukáže `model:` v meta řádku.
@@ -1379,6 +1551,7 @@ Běh 5 modelů s gate=3:
- Nasazeno `rsync`em na server, vlastník `nanobot:nanobot` ověřen. - Nasazeno `rsync`em na server, vlastník `nanobot:nanobot` ověřen.
**Co fungovalo a proč:** **Co fungovalo a proč:**
- Resolve proti reálnému serverovému configu: 7 presetů (`gemini-flash, gemini-flash-lite, glm, haiku, kimi, minimax, sonnet`), `kimi→kimi`, `flash-lite→gemini-flash-lite`, `gemini→ambiguous`, `nonsense→not found`. - Resolve proti reálnému serverovému configu: 7 presetů (`gemini-flash, gemini-flash-lite, glm, haiku, kimi, minimax, sonnet`), `kimi→kimi`, `flash-lite→gemini-flash-lite`, `gemini→ambiguous`, `nonsense→not found`.
- Fail-fast: `create-task.py --model nosuchmodel` → exit 1, hláška se seznamem, `inbox/` beze změny. - Fail-fast: `create-task.py --model nosuchmodel` → exit 1, hláška se seznamem, `inbox/` beze změny.
- **E2e:** task `model-test-kimi` (`--model kimi`, goal „reply PONG") doběhl za 3 s, log `START … preset=kimi`, výsledek `PONG`, `read-task` ukázal `model: kimi`. Tím ověřeno, že `bot._loop.set_model_preset` funguje i s **nainstalovaným** balíčkem nanobota (ne jen v upstream klonu). Testovací task zarchivován. - **E2e:** task `model-test-kimi` (`--model kimi`, goal „reply PONG") doběhl za 3 s, log `START … preset=kimi`, výsledek `PONG`, `read-task` ukázal `model: kimi`. Tím ověřeno, že `bot._loop.set_model_preset` funguje i s **nainstalovaným** balíčkem nanobota (ne jen v upstream klonu). Testovací task zarchivován.
@@ -1395,6 +1568,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Background tasky na silnějších modelech (M3/Kimi — na Ollama Cloud pomalé/verbózní) můžou u hlubokého researche přerůst 15 min. Uživatel nemá v příkazu řešit timeout, tak zvednout globální strop. **Cíl:** Background tasky na silnějších modelech (M3/Kimi — na Ollama Cloud pomalé/verbózní) můžou u hlubokého researche přerůst 15 min. Uživatel nemá v příkazu řešit timeout, tak zvednout globální strop.
**Co jsem zkusil:** **Co jsem zkusil:**
- Ověřil, že systemd `tasks-daemon.service` nemá vlastní start-timeout: `Type=oneshot``TimeoutStartUSec=infinity`, `RuntimeMaxUSec=infinity` (`systemctl --user show`). Jediný strop je tedy Python `asyncio.wait_for(TIMEOUT_SECONDS)`. - Ověřil, že systemd `tasks-daemon.service` nemá vlastní start-timeout: `Type=oneshot``TimeoutStartUSec=infinity`, `RuntimeMaxUSec=infinity` (`systemctl --user show`). Jediný strop je tedy Python `asyncio.wait_for(TIMEOUT_SECONDS)`.
- `TIMEOUT_SECONDS = 15*60 → 45*60` v `tasks-daemon.py`; sjednoceny zmínky „15 min" v docstringu, `architecture.md`, `knowledge.md`. - `TIMEOUT_SECONDS = 15*60 → 45*60` v `tasks-daemon.py`; sjednoceny zmínky „15 min" v docstringu, `architecture.md`, `knowledge.md`.
- Nasazeno `rsync`em, vlastník `nanobot:nanobot`, konstanta na serveru ověřena. Bez restartu (oneshot = nový proces na každý běh). - Nasazeno `rsync`em, vlastník `nanobot:nanobot`, konstanta na serveru ověřena. Bez restartu (oneshot = nový proces na každý běh).
@@ -1412,6 +1586,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Na dotaz „jak je m2.7 rychlý oproti glm-5.1" dát tvrdá apples-to-apples data — m2.7 zvažovaný jako náhrada za m3 (BLOCKED, viz knowledge.md). Předchozí streaming benchmark (téhož dne) měřil jen m3, ne m2.7. **Cíl:** Na dotaz „jak je m2.7 rychlý oproti glm-5.1" dát tvrdá apples-to-apples data — m2.7 zvažovaný jako náhrada za m3 (BLOCKED, viz knowledge.md). Předchozí streaming benchmark (téhož dne) měřil jen m3, ne m2.7.
**Co jsem zkusil:** **Co jsem zkusil:**
1. `/api/tags` na `nvidia.hell` ukázal jen `minimax-m3:cloud` (m2.7 z katalogu zmizel — Ollama Cloud ho nahradila m3). `POST /api/pull {"model":"minimax-m2.7:cloud"}` ale vrátil `{"status":"success"}` → cloud pointer **jde dotáhnout**, jen není v defaultním seznamu. 1. `/api/tags` na `nvidia.hell` ukázal jen `minimax-m3:cloud` (m2.7 z katalogu zmizel — Ollama Cloud ho nahradila m3). `POST /api/pull {"model":"minimax-m2.7:cloud"}` ale vrátil `{"status":"success"}` → cloud pointer **jde dotáhnout**, jen není v defaultním seznamu.
2. Streaming `/api/chat` benchmark (`/tmp/bench_ollama2.py`), identický `/remind list` prompt (8 položek do češtiny), **5 kol prokládaně** (každé kolo glm pak m2.7, ať sdílí stejnou zátěž cloudu) + mediány. TTFT = 1. content chunk; e2e = `eval_count / total`. 2. Streaming `/api/chat` benchmark (`/tmp/bench_ollama2.py`), identický `/remind list` prompt (8 položek do češtiny), **5 kol prokládaně** (každé kolo glm pak m2.7, ať sdílí stejnou zátěž cloudu) + mediány. TTFT = 1. content chunk; e2e = `eval_count / total`.
@@ -1423,6 +1598,7 @@ Běh 5 modelů s gate=3:
| minimax-m2.7 | 9,2 s | 11,3 s | 351 | **~28 tok/s** | | minimax-m2.7 | 9,2 s | 11,3 s | 351 | **~28 tok/s** |
**Co fungovalo a proč:** **Co fungovalo a proč:**
- Absolutní čísla nafouklá oproti rannímu m3 měření (glm TTFT 15 s vs ~6 s ráno) — cloud byl vytížený. Proto bráno jen jako **poměr v rámci běhu**, prokládání kol drží srovnatelnost. - Absolutní čísla nafouklá oproti rannímu m3 měření (glm TTFT 15 s vs ~6 s ráno) — cloud byl vytížený. Proto bráno jen jako **poměr v rámci běhu**, prokládání kol drží srovnatelnost.
- **Decode m2.7 ~3× pomalejší než glm** (~28 vs ~91 tok/s e2e) — dokonce **horší než m3** (~40 tok/s). Líný decode je rys celé MiniMax řady, m2.7 to nezlepšil. - **Decode m2.7 ~3× pomalejší než glm** (~28 vs ~91 tok/s e2e) — dokonce **horší než m3** (~40 tok/s). Líný decode je rys celé MiniMax řady, m2.7 to nezlepšil.
- **m2.7 doběhne dřív ve wall-clocku** (11 vs 18 s) **jen díky terseness** — vygeneroval ~4,5× méně tokenů (351 vs 1558). glm je upovídaný, ale per-token mnohem rychlejší. Konzistentní s MiniLoop 2026-06-03 (m2.7 4815 ms vs glm 1690 ms, tam podobné token county → glm vyhrál decodem). - **m2.7 doběhne dřív ve wall-clocku** (11 vs 18 s) **jen díky terseness** — vygeneroval ~4,5× méně tokenů (351 vs 1558). glm je upovídaný, ale per-token mnohem rychlejší. Konzistentní s MiniLoop 2026-06-03 (m2.7 4815 ms vs glm 1690 ms, tam podobné token county → glm vyhrál decodem).
@@ -1440,6 +1616,7 @@ Běh 5 modelů s gate=3:
**Cíl:** `/detach list` na minimax-m2.7 rozsypal formát — Running se vyrenderoval jako tabulka, Done/Failed jako syrové `| … |` roury (na GLM OK). Příčina: výstup relayuje LLM token po tokenu a markdown tabulka má dálkovou závislost (hlavička + `|---|`); slabý model u 2./3. sekce hlavičku zahodí. Cíl = formát robustní vůči LLM relay. **Cíl:** `/detach list` na minimax-m2.7 rozsypal formát — Running se vyrenderoval jako tabulka, Done/Failed jako syrové `| … |` roury (na GLM OK). Příčina: výstup relayuje LLM token po tokenu a markdown tabulka má dálkovou závislost (hlavička + `|---|`); slabý model u 2./3. sekce hlavičku zahodí. Cíl = formát robustní vůči LLM relay.
**Co jsem zkusil:** **Co jsem zkusil:**
- `render_table``render_list` v `tasks_common.py`: dvouřádková odrážka na úkol (slug · čas · stáří + odsazený popis), žádná tabulková gramatika. Popis se vynechá, když chybí `# Goal`. JSON zamítnut — jen posouvá křehkost na model, který ho převádí pro člověka. - `render_table``render_list` v `tasks_common.py`: dvouřádková odrážka na úkol (slug · čas · stáří + odsazený popis), žádná tabulková gramatika. Popis se vynechá, když chybí `# Goal`. JSON zamítnut — jen posouvá křehkost na model, který ho převádí pro člověka.
- `list-tasks.py` přepnut na `render_list`; `SKILL.md` `list` zpřísněn na „output verbatim, do not convert to a table". - `list-tasks.py` přepnut na `render_list`; `SKILL.md` `list` zpřísněn na „output verbatim, do not convert to a table".
- Testy přepsány (`test_render_list_*`, +`omits_goal_line_when_empty`) → **57 passed**. Lokální render proti dočasnému workspace ověřil starý i nový timestamp, soubor bez Goalu i neparsovatelný filename. - Testy přepsány (`test_render_list_*`, +`omits_goal_line_when_empty`) → **57 passed**. Lokální render proti dočasnému workspace ověřil starý i nový timestamp, soubor bez Goalu i neparsovatelný filename.
@@ -1459,12 +1636,14 @@ Běh 5 modelů s gate=3:
**Cíl:** Uživatel hlásil problém s user systemd službami `tasks-daemon.path` / `tasks-daemon.service`. Zjistit příčinu, navrhnout fix, nasadit po schválení. **Cíl:** Uživatel hlásil problém s user systemd službami `tasks-daemon.path` / `tasks-daemon.service`. Zjistit příčinu, navrhnout fix, nasadit po schválení.
**Co jsem zkusil / zjistil:** **Co jsem zkusil / zjistil:**
- `systemctl --user status` → obě unity `failed`, `.service` s `Result: unit-start-limit-hit`, „Start request repeated too quickly". - `systemctl --user status` → obě unity `failed`, `.service` s `Result: unit-start-limit-hit`, „Start request repeated too quickly".
- `stderr.log` + journal: v 18:19 daemon 5× po sobě spadl na `NameError: name 'LOG' is not defined` (`tasks-daemon.py:152`) → za <2 s vyčerpal default rate-limit (`StartLimitIntervalSec=10s`, `Burst=5`) → systemd zalatchoval **`.service` i `.path`**. - `stderr.log` + journal: v 18:19 daemon 5× po sobě spadl na `NameError: name 'LOG' is not defined` (`tasks-daemon.py:152`) → za <2 s vyčerpal default rate-limit (`StartLimitIntervalSec=10s`, `Burst=5`) → systemd zalatchoval **`.service` i `.path`**.
- Časová osa: do 17:29 OK (4× done); 17:44 task na `minimax`; **18:19:14 SIGTERM (status=143)** — žádný timeout v unitu (předchozí běhy i 1553 s doběhly), takže externí stop (deploy fixu); 18:19:3638 `.path` (level-triggered, inbox neprázdný) hned restartoval → 5× NameError → latch. - Časová osa: do 17:29 OK (4× done); 17:44 task na `minimax`; **18:19:14 SIGTERM (status=143)** — žádný timeout v unitu (předchozí běhy i 1553 s doběhly), takže externí stop (deploy fixu); 18:19:3638 `.path` (level-triggered, inbox neprázdný) hned restartoval → 5× NameError → latch.
- **Klíčové:** ten `NameError` už byl opravený předchozí session (import `LOG` na ř. 37, viz záznam výše) a fix byl na serveru nasazený. Ruční test daemonu s korektní PATH → **`exit=0`**. Příčina pádu pryč; jediný zbytek = systemd zaseknutý ve `failed`, který se sám nezotaví. - **Klíčové:** ten `NameError` už byl opravený předchozí session (import `LOG` na ř. 37, viz záznam výše) a fix byl na serveru nasazený. Ruční test daemonu s korektní PATH → **`exit=0`**. Příčina pádu pryč; jediný zbytek = systemd zaseknutý ve `failed`, který se sám nezotaví.
**Co fungovalo a proč:** **Co fungovalo a proč:**
- **A — recovery:** `systemctl --user reset-failed tasks-daemon.service tasks-daemon.path` + `restart tasks-daemon.path``.path` zpět `active (waiting)`. Inbox prázdný, nic se hned nespustilo. - **A — recovery:** `systemctl --user reset-failed tasks-daemon.service tasks-daemon.path` + `restart tasks-daemon.path``.path` zpět `active (waiting)`. Inbox prázdný, nic se hned nespustilo.
- **B — orphan:** smazán `failed/2026-06-07_18_28_43_…-gemini.md` (vznikl 18:28 po latchi, nikdy nezpracován — bez `## Result` sekce). - **B — orphan:** smazán `failed/2026-06-07_18_28_43_…-gemini.md` (vznikl 18:28 po latchi, nikdy nezpracován — bez `## Result` sekce).
- **C — hardening unitu** (`skills/detach/systemd/tasks-daemon.service`): přidáno `Restart=on-failure` + `RestartSec=60` (delay mezi pokusy po pádu; čistý exit 0 i SIGTERM od systemd nerestartují) a `StartLimitIntervalSec=1800` + `StartLimitBurst=20` (až 20 pokusů / 30 min, pak pauza + auto-resume jak okno klouže — už žádný permanentní latch ani ruční `reset-failed`). Doc ověřena: `man systemd.service` — pro `Type=oneshot` jsou zakázané jen `Restart=always/on-success`, `on-failure` povolený. - **C — hardening unitu** (`skills/detach/systemd/tasks-daemon.service`): přidáno `Restart=on-failure` + `RestartSec=60` (delay mezi pokusy po pádu; čistý exit 0 i SIGTERM od systemd nerestartují) a `StartLimitIntervalSec=1800` + `StartLimitBurst=20` (až 20 pokusů / 30 min, pak pauza + auto-resume jak okno klouže — už žádný permanentní latch ani ruční `reset-failed`). Doc ověřena: `man systemd.service` — pro `Type=oneshot` jsou zakázané jen `Restart=always/on-success`, `on-failure` povolený.
@@ -1481,6 +1660,7 @@ Běh 5 modelů s gate=3:
**Cíl:** `.gitignore` v serverovém repu (`~/.nanobot/workspace`) přibyl pozdě — `__pycache__/*.pyc` a `MEMORY.md.bak` se dostaly do gitu dřív, takže je ignore neřešil a pořád byly trackované. Vyndat je z gitu, nechat na disku. **Cíl:** `.gitignore` v serverovém repu (`~/.nanobot/workspace`) přibyl pozdě — `__pycache__/*.pyc` a `MEMORY.md.bak` se dostaly do gitu dřív, takže je ignore neřešil a pořád byly trackované. Vyndat je z gitu, nechat na disku.
**Co jsem zkusil:** **Co jsem zkusil:**
- `git ls-files -i -c --exclude-standard`**11 souborů** trackovaných i přes `.gitignore`: 10× `*.pyc` v `__pycache__/` (skills `detach`/`remind`) + `memory/MEMORY.md.bak`. - `git ls-files -i -c --exclude-standard`**11 souborů** trackovaných i přes `.gitignore`: 10× `*.pyc` v `__pycache__/` (skills `detach`/`remind`) + `memory/MEMORY.md.bak`.
- `git ls-files -i -c --exclude-standard -z | xargs -0 git rm --cached` → odstranění z indexu, soubory na disku zůstaly (ověřeno `ls`). - `git ls-files -i -c --exclude-standard -z | xargs -0 git rm --cached` → odstranění z indexu, soubory na disku zůstaly (ověřeno `ls`).
- Commit `f93c1cf` jen s těmito removaly (explicitní pathspec) — repo mělo rozdělanou autonomní práci (Dream procesor: `cron/jobs.json`, `memory/history.jsonl`, skripty `remind/`), té se commit nedotkl. Po commitu `git ls-files -i -c --exclude-standard` = **0**. - Commit `f93c1cf` jen s těmito removaly (explicitní pathspec) — repo mělo rozdělanou autonomní práci (Dream procesor: `cron/jobs.json`, `memory/history.jsonl`, skripty `remind/`), té se commit nedotkl. Po commitu `git ls-files -i -c --exclude-standard` = **0**.
@@ -1496,6 +1676,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Uživatel nechal nanobota přepsat `/remind` skill z `reminder.yaml` na SQLite. Zkontrolovat výsledek, posoudit funkčnost, otestovat, navrhnout změny (vč. textu SKILL.md). Nic neměnit bez souhlasu. **Cíl:** Uživatel nechal nanobota přepsat `/remind` skill z `reminder.yaml` na SQLite. Zkontrolovat výsledek, posoudit funkčnost, otestovat, navrhnout změny (vč. textu SKILL.md). Nic neměnit bez souhlasu.
**Co jsem zkusil:** **Co jsem zkusil:**
- Stáhl serverový skill (`rsync`) do `tmp/server-remind/`, přečetl `SKILL.md`, `db.py`, `remind_edit.py`, `remind_send.py`, `random_times.py` + všechny testy. - Stáhl serverový skill (`rsync`) do `tmp/server-remind/`, přečetl `SKILL.md`, `db.py`, `remind_edit.py`, `remind_send.py`, `random_times.py` + všechny testy.
- Ověřil reálný stav serveru: schema `reminders.sqlite` (nové: `days_filter/from_date/until_date`), 14 reálných připomínek migrováno (25 `at`, 6 cron, 9 random), `reminder_fires`=0. - Ověřil reálný stav serveru: schema `reminders.sqlite` (nové: `days_filter/from_date/until_date`), 14 reálných připomínek migrováno (25 `at`, 6 cron, 9 random), `reminder_fires`=0.
- Pustil test suite na serveru: **43 passed**. - Pustil test suite na serveru: **43 passed**.
@@ -1503,6 +1684,7 @@ Běh 5 modelů s gate=3:
- Live test doručení: `remind_edit.py add --at` na +2 min (id=15), ověřeno `reminder_fires` zápis `('at', delivered, 07:54:01)` + DELIVER v logu + reálný příchod na Telegram (potvrdil uživatel), pak `remove --keyword` (soft-delete). - Live test doručení: `remind_edit.py add --at` na +2 min (id=15), ověřeno `reminder_fires` zápis `('at', delivered, 07:54:01)` + DELIVER v logu + reálný příchod na Telegram (potvrdil uživatel), pak `remove --keyword` (soft-delete).
**Co fungovalo a proč:** Přechod na DB je funkčně nasazený a doručování jede. Nalezené defekty: **Co fungovalo a proč:** Přechod na DB je funkčně nasazený a doručování jede. Nalezené defekty:
- **P1 schedule_type collision:** `schedule_{at,cron,random}` mají vlastní AUTOINCREMENT id → překryv (at 125, cron 16, random 19). UNION-ALL inference v `remind_send.main()` označí každý cron/random fire jako `'at'``reminder_fires.schedule_type` špatně + dedup pro cron/random nefunkční (maskuje jen 60s tolerance). Fix: každá `_due_*` vrací svůj typ. - **P1 schedule_type collision:** `schedule_{at,cron,random}` mají vlastní AUTOINCREMENT id → překryv (at 125, cron 16, random 19). UNION-ALL inference v `remind_send.main()` označí každý cron/random fire jako `'at'``reminder_fires.schedule_type` špatně + dedup pro cron/random nefunkční (maskuje jen 60s tolerance). Fix: každá `_due_*` vrací svůj typ.
- **P1 stale bootstrap:** serverové `AGENTS.md` + `TOOLS.md` pořád mluví o `reminder.yaml` a starém formátu `reminder.log` (auto-load každý tah). - **P1 stale bootstrap:** serverové `AGENTS.md` + `TOOLS.md` pořád mluví o `reminder.yaml` a starém formátu `reminder.log` (auto-load každý tah).
- **P2 log regrese:** `log_operation` píše UTC + míchá ADD/EDIT/…/DELIVER; `TOOLS.md` „co dnes přišlo“ čeká Prague-time delivery-only. - **P2 log regrese:** `log_operation` píše UTC + míchá ADD/EDIT/…/DELIVER; `TOOLS.md` „co dnes přišlo“ čeká Prague-time delivery-only.
@@ -1521,6 +1703,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Provést odsouhlasené opravy z revize výše: P1 schedule_type, P1 bootstrap docs, P2 read-back přes reminder_fires, P3 cleanupy, repo+knowledge sync. **Cíl:** Provést odsouhlasené opravy z revize výše: P1 schedule_type, P1 bootstrap docs, P2 read-back přes reminder_fires, P3 cleanupy, repo+knowledge sync.
**Co jsem zkusil / udělal:** **Co jsem zkusil / udělal:**
- **remind_send.py:** každá `_due_{at,cron,random}` vrací `schedule_type`; smazána chybná UNION-ALL inference v `main()`. Odstraněn mrtvý `import yaml` + dep `pyyaml`. `CHAT_ID``_telegram_config()` čte `channels.telegram.allowFrom[0]` z configu, fallback konstanta. - **remind_send.py:** každá `_due_{at,cron,random}` vrací `schedule_type`; smazána chybná UNION-ALL inference v `main()`. Odstraněn mrtvý `import yaml` + dep `pyyaml`. `CHAT_ID``_telegram_config()` čte `channels.telegram.allowFrom[0]` z configu, fallback konstanta.
- **remind_edit.py:** helper `_resolve_one` (výběr přes `--id` nebo `--keyword`, ambiguous vypíše ids); `--id` přidáno k remove/edit/enable/disable; guard na `--replace-schedules` bez nového schedule; nový subcommand `delivered [--since]` (čte `reminder_fires`, Prague time); `from datetime import date` místo `__import__`; dep `croniter` only. - **remind_edit.py:** helper `_resolve_one` (výběr přes `--id` nebo `--keyword`, ambiguous vypíše ids); `--id` přidáno k remove/edit/enable/disable; guard na `--replace-schedules` bez nového schedule; nový subcommand `delivered [--since]` (čte `reminder_fires`, Prague time); `from datetime import date` místo `__import__`; dep `croniter` only.
- **SKILL.md:** instrukce odpovídat jazykem uživatele, dokumentace `delivered` + `--id` + duplicit, oprava matoucího Environment. - **SKILL.md:** instrukce odpovídat jazykem uživatele, dokumentace `delivered` + `--id` + duplicit, oprava matoucího Environment.
@@ -1544,6 +1727,7 @@ Běh 5 modelů s gate=3:
**Cíl:** Detailní revize skillu `/remind`, ověřit shodu lokál↔server, navrhnout a po schválení provést zlepšení (čistota, čitelnost, normy). **Cíl:** Detailní revize skillu `/remind`, ověřit shodu lokál↔server, navrhnout a po schválení provést zlepšení (čistota, čitelnost, normy).
**Co jsem zkusil / udělal:** **Co jsem zkusil / udělal:**
- **Review:** stáhl serverovou verzi, `diff` všech 5 skriptů + SKILL.md → **lokál identický se serverem**. Nálezy rozděleny P1/P2/P3, plán odsouhlasen uživatelem (rozsah: vše; `list` nemá vracet JSON). - **Review:** stáhl serverovou verzi, `diff` všech 5 skriptů + SKILL.md → **lokál identický se serverem**. Nálezy rozděleny P1/P2/P3, plán odsouhlasen uživatelem (rozsah: vše; `list` nemá vracet JSON).
- **P1 — korektnost:** `cmd_list` vypisoval **pozici** `{idx}.`, ne skutečné DB `id` → agent z `list` četl špatné číslo pro `--id`. Přepsáno na čitelný formát `#<id> text [status]` + odsazené schedule řádky, HH:MM okno, prázdný store → `(no active reminders)`. Helper `_schedule_lines`. SKILL.md popisuje nový formát místo neexistujícího „JSON". `db.log_operation(details: str)``str | None` (volá se s `None`). - **P1 — korektnost:** `cmd_list` vypisoval **pozici** `{idx}.`, ne skutečné DB `id` → agent z `list` četl špatné číslo pro `--id`. Přepsáno na čitelný formát `#<id> text [status]` + odsazené schedule řádky, HH:MM okno, prázdný store → `(no active reminders)`. Helper `_schedule_lines`. SKILL.md popisuje nový formát místo neexistujícího „JSON". `db.log_operation(details: str)``str | None` (volá se s `None`).
- **P2 — struktura:** `cmd_edit` validace (prázdný text, `_build_random`) přesunuta **před** `BEGIN` (žádná otevřená transakce na early-return). Duplicitní `_parse_window`/`_hhmm_to_minutes` v `remind_edit.py` smazány → reuse `parse_window` z `random_times.py` (publikováno odebráním podtržítka). `_build_random` se volá jen jednou — `_insert_schedules` dostává hotový `random_cfg`. - **P2 — struktura:** `cmd_edit` validace (prázdný text, `_build_random`) přesunuta **před** `BEGIN` (žádná otevřená transakce na early-return). Duplicitní `_parse_window`/`_hhmm_to_minutes` v `remind_edit.py` smazány → reuse `parse_window` z `random_times.py` (publikováno odebráním podtržítka). `_build_random` se volá jen jednou — `_insert_schedules` dostává hotový `random_cfg`.
@@ -1553,6 +1737,439 @@ Běh 5 modelů s gate=3:
**Co fungovalo a proč:** Skilly se čtou bez restartu. `reminder_cron.log` mtime 07:36 (před deployem) = od deploye crontab sender nezapsal žádnou novou chybu → běží čistě. Staré traceby v logu pochází z dávno mrtvé verze (`_process_reminder`, sloupce `days/start_date/end_date`, `ROLLBACK` v main) — irelevantní. **Co fungovalo a proč:** Skilly se čtou bez restartu. `reminder_cron.log` mtime 07:36 (před deployem) = od deploye crontab sender nezapsal žádnou novou chybu → běží čistě. Staré traceby v logu pochází z dávno mrtvé verze (`_process_reminder`, sloupce `days/start_date/end_date`, `ROLLBACK` v main) — irelevantní.
**Co zbývá / gotcha:** **Co zbývá / gotcha:**
- **`log_operation` ignoruje `REMIND_DB`** — píše vždy do reálného `workspace/log/reminder.log` přes `__file__`-relativní cestu. Spuštění test suite **na serveru** proto zapsalo 24 fixture řádků (timestamp `2026-06-10T06:47:32`) do reálného logu. **Poučení: testy spouštět jen lokálně.** Úklid logu (odstranit 24 řádků + truncate staré traceby v `reminder_cron.log`) byl blokován auto-classifierem (neautorizovaný prod write) — čeká na svolení uživatele. - **`log_operation` ignoruje `REMIND_DB`** — píše vždy do reálného `workspace/log/reminder.log` přes `__file__`-relativní cestu. Spuštění test suite **na serveru** proto zapsalo 24 fixture řádků (timestamp `2026-06-10T06:47:32`) do reálného logu. **Poučení: testy spouštět jen lokálně.** Úklid logu (odstranit 24 řádků + truncate staré traceby v `reminder_cron.log`) byl blokován auto-classifierem (neautorizovaný prod write) — čeká na svolení uživatele.
**Jak vrátit zpět:** `git revert <commit>` skillu + rsync předchozí verze na server. DB schema/data beze změny. **Jak vrátit zpět:** `git revert <commit>` skillu + rsync předchozí verze na server. DB schema/data beze změny.
## 2026-06-10 09:50 — Remind skill úklid: rename `remind_edit.py`→`remind_cli.py`, edit-guidance, hybrid SKILL.md
**Cíl:** Tři papercuts najednou (jeden deploy): (1) zavádějící název `remind_edit.py` (dělá i `list`/`delivered`, read-only); (2) agent při opravě textu maže+vytváří záznam znovu, místo `edit --text` (skript to **už uměl**, jen SKILL.md k tomu nenaváděl); (3) SKILL.md duplikoval výčet flagů s argparse → drift. Plán: [plans/](plans/) (schválen v plan mode).
**Co jsem zkusil / udělal:**
- **A. Rename** `git mv remind_edit.py → remind_cli.py` + `test_remind_edit.py → test_remind_cli.py` (import + `remind_cli.*` + argv[0]). Cross-importy nedotčené (`remind_send` importuje jen z `db`/`random_times`). **Crontab nezměněn** — spouští `remind_send.py`.
- **B. Dedup:** `_minutes_to_hhmm` byl doslova zkopírovaný v cli i send → přesunut do `random_times.py` jako veřejné `minutes_to_hhmm`, obě kopie smazány, importy upraveny.
- **C. Edit-guidance + test:** SKILL.md explicitně „oprava textu = `edit --id <n> --text`, NIKDY remove+add"; nový test `test_edit_text_by_id` (pokrývá dřív netestovanou `--id` větev editace).
- **D. SKILL.md → hybrid:** zachován NL→příkaz mapping + behaviorální kontrakt (formát `list`, JSON mutace, keyword/id ambiguous, `delivered`); vyčerpávající výčet flagů → `<cmd> --help`. Řeší drift z [memory.md](memory.md).
- Lokální změny delegovány na sonnet agenta; deploy + verifikaci řídil opus.
**Co fungovalo a proč:**
- Lokálně **52 passed** (51 + nový test).
- Server = zdroj pravdy: `rsync` server→`tmp/server-remind/`, `diff` všech 10 zdrojů proti **HEAD****identické**, žádná autonomní divergence (Dream procesor nesahal) → deploy bezpečný.
- Deploy `rsync` (exclude `__pycache__`/`.pytest_cache`/`.venv`), stale `remind_edit.py` + starý test **explicitně `rm`** (rsync bez `--delete` je nechává). Owner `nanobot:nanobot` OK.
- Server `remind_cli.py --help` + `list` (reálná data) OK; `remind_edit.py` pryč; crontab dál `remind_send.py`.
- **Živý edit test (bez Telegram fire):** `add --at 2020-01-01` (v minulosti → nikdy due), `edit --id 17 --text "TEST opraveny text"` → text změněn, `at` schedule (id 27) **zachován**, pak `remove --id 17` (úklid).
**Co zbývá:** Commit + push (tracking zvlášť dle konvence). `tmp/server-remind/` je v `.gitignore`.
**Jak vrátit zpět:** `git revert <commit>` skillu + na serveru rsync předchozí verze a `mv remind_cli.py remind_edit.py` zpět (DB beze změny).
## 2026-06-13 07:48 — note skill: zákaz vymýšlení tagů
**Cíl:** Odstranit nešvar, kdy agent při zakládání poznámky automaticky vymýšlí tagy z obsahu. Tagy se mají přidat jen když je uživatel explicitně napíše (první token za triggerem).
**Co jsem zkusil:** Příčina čistě v textu `skills/note/SKILL.md` — skript `note.py` je v pořádku (`--tags` volitelný, default `[]`, žádné auto-tagování). Svádějící formulace: hlavička „extract tags", věta „Tags are created automatically on first use", krok 1 Write protocolu „Extract inline tags". Upravil jsem všechny tři: hlavička → „take only explicitly-typed tags"; do Tag protocolu přidáno tvrdé pravidlo, že tagy pochází *jen* z prvního tokenu, nikdy se neodvozují z obsahu; přeformulováno „DB accepts any new tag without prior registration — not a license to make up tags"; krok 1 → „Take … only … tags field stays empty — never fill it from the content".
**Co fungovalo a proč:** Server↔lokál před úpravou shoda (diff prázdný). Nasazeno `rsync -av skills/note/ nanobot@nanobot.hell:…/skills/note/`, vlastník `nanobot:nanobot` ověřen. Skilly se čtou každý tah bez restartu.
**Co zbývá:** Verifikace reálným chováním (poznámka bez explicitního tagu → žádné tagy). Commit tracking zvlášť.
**Jak vrátit zpět:** `git revert <commit>` + rsync předchozí verze SKILL.md na server. DB beze změny.
## 2026-06-13 10:25 — Display ID (přečíslování on-the-fly) do /remind a /bookmark
**Cíl:** Přenést mechanismus z `/note` (sekvenční display ID 1..N počítané on-the-fly, nezávislé na rozházených interních DB id, přečíslování po každém smazání → snazší mazání/úpravy podle krátkého čísla) do `/remind` a `/bookmark`. Uživatel řekl „/article", ale to je čistě sumarizační skill bez seznamu — potvrzeno, že myslel `/bookmark`. Plán: [plans/](../.claude/plans/) (schválen v plan mode).
**Co jsem zkusil / udělal:**
- **Vzor z note (zrcadlit, ne vymýšlet):** helper vrací seřazené interní id aktivní množiny → výpis postaví `{interní_id: pořadí}` → mutace berou display ID, `idx = n-1`, bounds-check, přeloží na interní id.
- **remind** (`remind_cli.py`, `forecast.py`): nový `_active_display_order` (řazení `ORDER BY id`); `cmd_list` tiskne `#{display}`; **load-bearing: `--id` u remove/edit/enable/disable přeznačeno z interního DB id na display ID** + `--keyword` beze změny; `_resolve_one` bounds-check (`{"error":"no match","display_id":n}`), nejednoznačný keyword vrací `matches:[{"display_id":…}]`; `upcoming` + `format_upcoming(fires, id_to_display)` tisknou stejné display ID jako `list`; interní id zmizelo z user-facing výstupu (zůstává v audit logu a v JSON mutací).
- **bookmark** (`bookmark.py`): dvě množiny — `_ordered_ids(read=False)` nepřečtené (`created_at DESC`), `read=True` přečtené (`read_at DESC`); `read`/`show` řeší proti `list`, `unread` proti `history`; `_print_bookmark(row, display_id)`; `cmd_list` počítá display ID nad **celou** nepřečtenou množinou (filtr `--tag` drží globální čísla → očekávané mezery, jako note); `cmd_add` echo `#1` (newest-first); odstraněn dead `bid`.
- SKILL.md obou skillů aktualizovány (EN-only): display ID = sekvenční, on-the-fly, přečíslovává se; který výpis pro který příkaz; interní id se neukazuje.
- Testy remind: `test_forecast.py` opraven na novou signaturu `format_upcoming` (+ ověřuje překlad interní→display); přidány `test_display_id_renumbers_after_remove`, `test_id_out_of_range_reports_display_id`, `test_ambiguous_keyword_returns_display_ids`.
**Co fungovalo a proč:**
- Lokálně **71 passed** (`uv run --with pytest --with croniter pytest`).
- bookmark nemá testy → ruční ověření přes import s patchnutým `DB_PATH` na temp DB: add→#1, list 1..3 newest-first, `read 2` → přečíslování, `show 1`, `--tag` global ID, `history` vlastní číslování, `unread 1` přes history pořadí. Vše dle návrhu.
- Server = zdroj pravdy: `rsync` server→`tmp/srv/`, diff všech zdrojů proti **HEAD** → identické (žádná autonomní divergence Dreamem) → deploy bezpečný.
- Deploy `rsync` (exclude `__pycache__`); omylem se nahrál `.pytest_cache/` → smazán ze serveru. Owner `nanobot:nanobot` OK. Smoke test server `remind list` → display ID #1#4 (interní id rozházená po smazáních). Skilly se čtou bez restartu.
**Co zbývá:** Commit kódu skillů (tracking zvlášť dle konvence — řeší uživatel). Návrh zápisu do `decisions.md`: load-bearing volba „`--id` u remind mutací = display ID, ne interní DB id".
**Jak vrátit zpět:** `git revert <commit>` skillů + rsync předchozí verze (HEAD) na server. DB schema/data beze změny.
## 2026-06-13 19:24 — Detach: recovery démona + úklid (code-review fixy #1,#2,#4,#5,#6,#7)
**Cíl:** Opravit 6 nálezů z code-review skillu `/detach` — dvě reálné díry v recovery démona (#1 osiřelé `running/`, #2 poison-loop) a čtyři úklidové (#4 duplicitní CONFIG, #5 duplicitní ensure_queue_dirs, #6 Markdown notifikace posílaná jako plain text, #7 duplicitní řazení v read-task). Plán: [plans/](../.claude/plans/) (schválen v plan mode). Implementaci lokálních editů dělal sonnet subagent (konvence „plány vykonává sonnet"), opus revize + deploy.
**Co jsem zkusil / udělal:**
- **#1+#2 společný refactor** `tasks-daemon.py`: vytažen `finalize_task(...)` (zápis Result+meta, mv do done/failed, resolve chat_id v `try`, guardovaná notifikace) — sdílí ho `process_task` i nový `reclaim_orphans()`. `process_task` přeskládán na **move-first** (resolve chat_id už neběží před mv ani mimo try → konec poison-loopu u ne-telegram kanálu s vadným `allowFrom`). `reclaim_orphans()` na startu `main()` označí přežité `running/*.md` jako `failed` + `(INTERRUPTED…)` + best-effort notifikace.
- **#1d** `tasks-daemon.path`: přidán druhý `DirectoryNotEmpty=…/tasks/running`, aby reclaim nečekal na další inbox task.
- **#4** `CONFIG` importováno z `tasks_common` (smazána lokální definice). **#5** `QUEUE_DIRS` + `ensure_queue_dirs()` do `tasks_common`, použito v create-task i daemonu. **#6** notifikace bez zpětných apostrofů. **#7** `find_matches` reuse `completed_files()`.
- **Drift při porovnání se serverem:** server měl `TIMEOUT_SECONDS = 20 * 60` (repo/HEAD = 45), upraveno mimo repo. Per „server je zdroj pravdy" zachováno 20 min, dotaženo do repa **samostatným commitem** (5581411) PŘED refactorem. Potvrzeno uživatelem.
**Co fungovalo a proč:**
- Lokálně **57 passed** (`uv run --with pytest pytest`).
- Server = zdroj pravdy: `rsync` server→`tmp/detach-server/`, diff proti HEAD → jediný drift `tasks-daemon.py` (timeout). Ostatní soubory identické.
- Deploy `rsync` (exclude `__pycache__`, `.pytest_cache`). `.path` unit je **symlink** z `~/.config/systemd/user/` do skill dir → rsync ho aktualizoval; `systemctl --user daemon-reload` + `restart tasks-daemon.path` (active). Owner `nanobot:nanobot` OK.
- **Live #1 verifikace:** dummy `running/2026-06-13_11_30_00_000001-reclaim-test.md` (chat_id `"1"` = neplatný, ať nespamuje reálný Telegram) → nový `running/` watch spustil démona → reclaim → přesun do `failed/` se `status: failed` + `(INTERRUPTED…)`. Log: `RECLAIM` + `END status=failed` + `NOTIFY-FAILED HTTP 400` (guard zafungoval). Test artefakt smazán z `failed/`.
- **#2** ověřeno strukturálně (resolve uvnitř finalize po mv, v try) + reclaim test exercoval guardovanou notify cestu; live failure-injection na prod config zbytečně invazivní.
**Co zbývá:** Návrh zápisu do `decisions.md` (load-bearing, čeká na souhlas autora): (a) `.path` unit hlídá i `running/` jako self-heal mechanismus reclaimu; (b) osiřelý task → `failed` (ne retry/re-queue) kvůli neopakování side-effectů agenta.
**Jak vrátit zpět:** `git revert 999b33c` (refactor) a/nebo `git revert 5581411` (timeout) + rsync předchozí verze (HEAD) na server; obnovit původní `.path` unit (jen inbox watch) + `daemon-reload` + `restart tasks-daemon.path`. DB/tasks data beze změny.
## 2026-06-14 08:46 — note: explicitní registrace tagů (konec auto-create)
**Cíl:** `/note` už nesmí tagy zakládat implicitně prvním použitím. Vznik tagu = samostatný explicitní příkaz; neznámý tag při zakládání poznámky → skill se zeptá, zda ho založit.
**Co jsem zkusil / udělal:**
- `note.py`: nová tabulka `tags(name PRIMARY KEY, created_at)`; `_backfill_tags` v migraci nasází distinct tagy z existujících poznámek (jen když je registr prázdný — idempotentní).
- `cmd_add`: po formát-validaci kontrola proti registru; neznámý tag → stderr `Unknown tag(s): …` + **exit 2**, poznámka se NEUKLÁDÁ. Exit kódy: 0 ok / 1 špatný formát / 2 neznámý tag.
- Nové příkazy `tag-add NAME` (idempotentní, reuse `_validate_tags`) a `tag-list` (řazení podle jména, bez display ID — tagy se referencují jménem; mazání se nezavádí).
- `SKILL.md`: přepsán Tag protocol (registrace povinná), Write protocol (větev na exit 2 — Ano→`tag-add`+re-run, Ne→re-run bez tagu), nová sekce Tag management, edge case `tag add` bez jména.
**Co fungovalo a proč:** Lokální verifikace na throwaway kopii (`tmp/verify`, real db naseedovaná) — backfill (arch/cli/test), known tag exit 0, unknown exit 2 + neuloženo, `tag-add` idempotentní, špatný formát exit 1, mixed known+unknown exit 2. Server je zdroj pravdy: před nasazením `rsync` serverové verze do `tmp/` + diff = jen mé změny (žádný Dream edit k záchraně). Nasazeno `rsync -av`, vlastník `nanobot:nanobot`. Migrace+backfill na serveru proběhla při prvním `tag-list` — zaregistrováno 5 tagů (chata/devops/dt-glass/shop/test). `uv` na serveru je v `~/.local/bin/uv` (není na non-interactive PATH).
**Co zbývá:** Návrh zápisu do `decisions.md` (čeká na souhlas autora): (a) registr tagů jako separátní tabulka + `add` odmítá neznámé (exit 2), vznik výhradně `tag-add` — load-bearing; (b) tagy se referencují jménem (bez display ID), mazání se nezavádí; (c) backfill jen při prázdném registru.
**Jak vrátit zpět:** `git revert <tento commit kódu>` + `rsync -av` předchozí verze `skills/note/` na server. Tabulka `tags` v DB může zůstat (stará verze ji ignoruje); pokud vadí: `sqlite3 db/note.sqlite "DROP TABLE tags;"` lokálně i na serveru.
## 2026-06-14 10:11 — note: render-safe URL ve `list` (inline Markdown odkaz)
**Cíl:** Ve `/note list` se u poznámky s holými URL (#7 „DT GLASS") v chat UI nevykreslilo pořadové číslo. Diagnóza (read-only): skript i data jsou OK — `note.py list` tiskne `N.` správně a model dostává správný text. Rozbíjí to až render chatu: řádek je položka číslovaného seznamu a holé `https://` URL se zároveň autolinkne do rich-link „chipu" (favicon „DG"), který se vykreslí na začátek řádku a překryje marker čísla. Ostatní řádky bez URL OK.
**Co jsem udělal:**
- `note.py`: nový helper `_linkify(text)` — regexem `https?://\S+` obalí holé URL do inline Markdown odkazu `[url](url)` (inline odkaz se nerenderuje jako chip → číslo přežije, URL zůstane celé i klikací). Trailing-punctuation peel (`.,;:!?)]`) drží čárku/závorku za URL mimo cíl odkazu.
- Použito jen v `cmd_list` (řádek výpisu). `cmd_show` záměrně netknut — má verbatim kontrakt; navíc není číslovaný seznam, ke kolizi nedochází. `add`/`delete` jednořádkové, bez kolize.
- `SKILL.md` List protocol: věta, že skript URL vypisuje jako inline odkazy a model má echovat beze změny (ať nepřebaluje zpět na holé URL).
**Co fungovalo a proč:** Regex izolovaně lokálně (čárka i `)` mimo odkaz). Server = zdroj pravdy: `rsync` server→`tmp/` + diff `note.py`/`SKILL.md` proti HEAD = IDENTICAL (žádný Dream edit k záchraně). Nasazeno `rsync -av skills/note/`, vlastník `nanobot:nanobot`. Server `note.py list` po deployi: #7 má URL ve tvaru `[https://…](https://…)`, marker `7.` na začátku. Prezentační změna — DB obsah i display-ID logika beze změny.
**Co zbývá:** Reálné ověření renderu v nanobot UI na konkrétním modelu dělá uživatel (zda inline odkaz UI neunfurluje do chipu). Fallback při unfurlu: backtick `` `url` `` (bez klik) nebo zkrácený popisek `[odkaz](url)`. Změna kódu `skills/note/` zatím necommitnuta (čeká na pokyn).
**Jak vrátit zpět:** `git checkout HEAD -- skills/note/scripts/note.py skills/note/SKILL.md` + `rsync -av skills/note/` předchozí verze na server. Bez dopadu na DB.
## 2026-06-14 11:33 — note: URL ve `list` — revert `_linkify`, model formátuje (bare URL na vlastní řádky)
**Cíl:** Dotáhnout render `/note list` u poznámek s URL tak, aby bylo vidět pořadové číslo **i** klikací odkaz. Předchozí iterace (`_linkify` → `[url](url)`, viz záznam 10:11) v reálném testu selhala stejně jako holé URL.
**Klíčové zjištění (přímý test s nanobot agentem):** Chyba je v markdown rendereru UI, ne v textu. UI **slučuje dva sousední inline odkazy `[text](url)`** do jednoho klikacího bloku a druhý URL schová → přebije i číslo položky. Naopak **holé URL se renderují správně a jsou klikací** (autolink); když je každý URL na vlastním řádku / pod odrážkou, neslučují se a číslo zůstane. `_linkify` (`[url](url)`) tedy dělal přesně tu rozbíjející věc.
**Co jsem udělal:**
- `note.py`: smazán helper `_linkify`; `cmd_list` vrácen na čistý výpis `N. {content}{tags}` (bare URL, jeden řádek). `cmd_show`/`add`/`delete` netknuty, DB i display-ID logika beze změny.
- `SKILL.md` List protocol: místo věty o „inline Markdown links" nová sekce *URLs in a note* s příkladem vstup→výstup — model má každý URL dát na vlastní řádek pod odrážku, **holý (nikdy `[text](url)`)**, popisek spárovat s URL, číslo i `[#tag]` zachovat. Důvod (slučování sousedních inline odkazů) ve skillu stručně.
- **Architektura:** formátování dělá model dle příkladu (umí spárovat popisek s URL). Deterministické formátování ve skriptu (zlom před každý URL) drženo **v záloze** v plánu pro případ, že glm příklad nedodrží.
**Co fungovalo a proč:** Server = zdroj pravdy: `rsync` server→`tmp/note-server` + diff proti repu = IDENTICAL (žádný Dream edit). `ast.parse` OK, `_linkify` pryč, `re` stále používáno (`_TAG_RE`). Nasazeno `rsync -av skills/note/`, vlastník `nanobot:nanobot`. Server `note.py list`: #7 „DT GLASS" má bare URL na jednom řádku, marker `7.` na začátku, žádný `[url](url)`.
**Co zbývá:** Reálné ověření v nanobot UI dělá uživatel — zda model (glm) příklad dodrží a render ukáže číslo + klikací URL pod odrážkami; otestovat i konzoli/Telegram. Když glm příklad nedodrží → přepnout na zálohu (deterministika ve skriptu). Změna kódu `skills/note/` zatím necommitnuta (čeká na pokyn).
**Jak vrátit zpět:** `git checkout HEAD -- skills/note/scripts/note.py skills/note/SKILL.md` + `rsync -av skills/note/` předchozí verze na server. Bez dopadu na DB.
## 2026-06-14 11:39 — note: URL ve `list` — fallback do skriptu (model příklad nedodržel)
**Cíl:** Přístup „model formátuje dle SKILL příkladu" v reálném testu selhal — glm vypsal raw řádek beze změny (jeden řádek, holé URL), UI ho celý udělalo klikací a číslo přebilo. Přepnuto na zálohu: formátování deterministicky ve skriptu.
**Co jsem udělal:**
- `note.py`: helper `_urls_on_own_lines(text)` + regex `_LABELED_URL_RE` (`\s*([^\s,]+:\s*)?(https?://[^\s,]+)`) — každý URL i s předsazeným „Label:" tokenem (pokud je) na vlastní odrážku ` - `. URL zůstává holé (autolink), comma/space url neukončuje předčasně (`[^\s,]+`). Použito jen v `cmd_list`.
- `SKILL.md` *URLs in a note*: model už neformátuje — **echovuje verbatim** (skript dodá odrážky); zákaz collapse zpět na jeden řádek a `[text](url)` zůstává.
- Heuristika popisku: „Label:" se spáruje s URL jen když je těsně před ním (jen `\s*` mezi) — vzdálený dvojtečkový token (např. „poznámka: … url") se nechytí. Cena: u prvního URL zůstane oddělovač („—") viset na konci hlavičkového řádku; přijatelné.
**Co fungovalo a proč:** Lokální test helperu na 4 vstupech (dvě labeled URL, bez URL, url uprostřed věty, jediný url) — výstup čistý, popisky spárované, no-URL poznámka beze změny. `ast.parse` OK. Nasazeno `rsync -av`, vlastník `nanobot:nanobot`. Server `note.py list`: #7 „DT GLASS" má hlavičku + dvě odrážky `- Universal: <url>` / `- Amber: <url>`, číslo `7.` na začátku.
**Co zbývá:** Reálné ověření v nanobot UI dělá uživatel (číslo + klikací URL pod odrážkami; ať model echovuje verbatim a neslepí to). Otestovat i konzoli/Telegram. Změna kódu `skills/note/` zatím necommitnuta (čeká na pokyn).
**Jak vrátit zpět:** `git checkout HEAD -- skills/note/` + `rsync -av skills/note/` předchozí verze na server. Bez dopadu na DB.
## 2026-06-11 20:02 — SOUL.md: přidány 2 behaviorální principy + zachycena autonomní divergence serveru
**Cíl:** Uživatel zvažoval přidat do nanobotových instrukcí blok behaviorálních pravidel (simplest approach, don't assume, surface tradeoffs, minimal code, loop-until-verified). Posoudit přínos a případně přidat.
**Co jsem zkusil / udělal:**
- Analýza navrženého textu proti `SOUL.md`/`AGENTS.md` (oba se čtou do system promptu každý tah, fresh — viz knowledge.md). Závěr: ~80 % už pokryto v SOUL.md (execute-immediately ř.19, don't-assume ř.21, ask-when-unsure ř.10/23/27, verify-after-changes ř.24). Coding-specific body (minimal code, surgical diffs, clean up orphans) pro asistenta irelevantní. Reálně nové jen 2: **jednoduchost** a **surface tradeoffs**.
- Stáhl serverové `SOUL.md`+`AGENTS.md` (`rsync`→`tmp/`). `diff` serveru proti `workspace/SOUL.md` (repo).
- **Server autonomně divergoval od posledního sync (`7d89c56`):** zahodil 3 řádky (`Předpokládám znalost u uživatele…`, `U nevratných akcí raději počkat na potvrzení`, `Žádné emojis…`) a přidal 1 (`Čísla, limity, kvóty… ověřuj na primárním zdroji`). Pravděpodobně Dream procesor.
- Dle pravidla „server = zdroj pravdy": server vyhrává, 3 zahozené řádky **nekřísím**. Mé 2 řádky přidal do serverové kopie:
- `## Hodnoty`: „Jednoduchost před komplexností — nejjednodušší funkční řešení; složitější vzory nenavrhuj, dokud o ně uživatel neřekne"
- `## Pravidla provedení`: „Když existuje víc cest, vynes tradeoff nahlas místo tichého výběru jedné"
- `rsync` zpět na server, ověřen owner `nanobot:nanobot` + oba řádky (ř.14, ř.26). Repo `workspace/SOUL.md` dotaženo na nový serverový stav (vč. autonomních změn serveru).
**Co fungovalo a proč:** SOUL.md se čte fresh každý tah → **bez restartu** platí od příští zprávy. Porovnání se serverem odhalilo autonomní divergenci dřív, než by ji slepý přepis lokální verzí zahodil.
**Co zbývá:** Zvážit, zda 3 řádky zahozené serverem (zvlášť „U nevratných akcí raději počkat na potvrzení") chceme obnovit, nebo respektovat rozhodnutí Dreamu. Zatím ponecháno tak, jak je server nastavil.
**Jak vrátit zpět:** `git revert <commit>` + rsync předchozí verze na server.
## 2026-06-11 20:06 — SOUL.md: obnoven řádek o potvrzení nevratných akcí
**Cíl:** Z 3 řádků, které server autonomně zahodil (viz záznam 20:02), obnovit ten bezpečnostně relevantní.
**Co jsem udělal:** Posoudil všechny 3 zahozené řádky proti zbytku SOUL.md. Jen `U nevratných akcí raději počkat na potvrzení` byl skutečná mezera (ostatní 2 — „předpokládám znalost", „žádné emojis" — pokryto `bez ozdob`/terseness/USER.md, ponechány zahozené). Obnoven do `## Pravidla provedení` k pravidlu o potvrzování vícekrokových úkolů, přeformulován obecněji: „U nevratných akcí (smazání, přepis, odeslání) raději počkej na potvrzení — i u jednokrokových". `rsync` na server, ověřen owner + řádek (ř.22), repo dotaženo.
**Co fungovalo a proč:** Fresh read každý tah → bez restartu.
**Jak vrátit zpět:** `git revert <commit>` + rsync předchozí verze na server.
## 2026-06-15 05:30 — /remind: náhodný rozvrh napříč týdnem + sync display-id featury ze serveru
**Cíl:** Rozšířit `/remind` o „N× náhodně v týdnu" (vedle stávajícího N× za den) — N náhodných odpálení rozprostřených přes celý týden.
**Co jsem zkusil / udělal:**
- **Návrh:** engine `compute_fire_times(target_date, text, cfg)` zůstává **per-day volaný** — pro weekly režim spočítá celý týdenní plán (seed = pondělí týdne + text, ne den) a vrátí jen fires daného dne. Tím daemon i forecast nemění iterační logiku. Semantika (potvrzeno uživatelem): N **různých** dnů (PoNe), 1 čas na den, okno povinné; `min(count, dostupné dny)` clamp pro hraniční týdny.
- **Změny:** sloupec `schedule_random.period` ('day'/'week', default 'day') + idempotentní `_migrate(conn)` v `get_db` (PRAGMA-guarded ALTER, protože `init_db` na živou DB nesahá); `_weekly_fire_times` v `random_times.py`; flag `--random-times-per-week` (add+edit, mutually exclusive s `--random-times-per-day`); `period` do INSERT/SELECT/list výpisu (`× weekly`/`× daily`). +10 testů.
- **Server = zdroj pravdy (klíčový krok):** `rsync` server→`tmp/server-remind/`, `diff` proti **git HEAD** odhalil, že server **autonomně divergoval** — měl feature **display IDs** (přečíslování reminderů na 1-based pozici v `list`/`upcoming`, `--id` i chybové hlášky pracují s display ID), která **nebyla v repu**. Pravděpodobně Dream procesor.
- Postup dle pravidla: weekly diff uložen jako patch (`git diff HEAD > /tmp/weekly.patch`), pracovní strom resetován na HEAD, serverové verze 5 divergovaných souborů (`SKILL.md`, `remind_cli.py`, `forecast.py`, `test_remind_cli.py`, `test_forecast.py`) přeneseny do repa → **commit „sync display IDs"** (71 testů). Pak `git apply --3way` weekly patche — všechny `.py` čistě, konflikt jen v `SKILL.md` (oba odstavce do stejného místa, ponechány oba) → **commit weekly** (80 testů).
- **Deploy:** `rsync` (exclude `__pycache__`/`.venv`/`.pytest_cache`), owner `nanobot:nanobot` OK. `list` na serveru → prod DB se zmigrovala (`period` přidán), stávajících 13 reminderů dál `× daily`.
**Co fungovalo a proč:** Per-day volací rozhraní enginu = daemon/forecast bez změny logiky (jen `sr.period` do SELECTů, protože `random_cfg_from_row` ho čte). Porovnání se serverem proti **HEAD** (ne proti pracovnímu stromu, kde už byly mé editace) odhalilo cizí divergenci — slepý rsync by display-id feature smazal. Dvoukrokový merge (sync commit → weekly na něj) zachoval obě featury, ověřeno 80 zelenými testy + kombinovaným smoke testem (display IDs #1/#2 + `2× weekly` + 2 různé dny).
**Co zbývá:** Commit tracking souborů. Skripty se čtou per-invocation → daemon bere nový kód bez restartu.
**Jak vrátit zpět:** `git revert 17feee6` (weekly) a `git revert fcfe794` (display-id sync), pak `rsync` předchozí verze skillu na server. Sloupec `period` v prod DB lze nechat (default 'day' = původní chování) nebo `ALTER TABLE schedule_random DROP COLUMN period`.
## 2026-06-15 05:44 — /remind: vytažení data-access vrstvy do `store.py`
**Cíl:** Reakce na uživatelovu otázku, zda `remind_cli.py` (534 ř.) není moc dlouhý. Po re-review ne kvůli délce, ale kvůli dvěma smellům: (1) `db.py` deklarovaný jako storage layer neobsahoval žádné dotazy — veškeré SQL žilo v CLI; (2) transaction boilerplate (`get_db`/`BEGIN`/`COMMIT`/`ROLLBACK`/`close`) duplikovaný ~7× napříč `cmd_*`. Cíl: CLI = čistá prezentační vrstva, SQL + lifecycle do nového modulu. Veřejné API beze změny (testy musí projít).
**Co jsem zkusil:** Plán (`plans/` přes plan mode), implementaci delegoval na sonnet agenta dle konvence. Nový `scripts/store.py` (240 ř.): context managery `connection()`/`transaction()` + dotazové funkce (`insert_reminder`, `insert_schedules`, `fetch_reminder`, `schedules_for`, `list_active`, `find_active_by_id/keyword`, `active_display_order`, `soft_delete`, `update_text`, `delete_schedules`, `touch`, `set_enabled`, `delivered_since/today`). `remind_cli.py` 534→400 ř., všechny `cmd_*` přes `store.*`, žádné inline SQL ani `BEGIN/COMMIT`. `remind_send.py` zatím nedotčen (volitelný krok).
**Co fungovalo a proč:** 80/80 testů zelených (`uv run --with croniter --with pytest pytest`), smoke add→list→remove OK. **Past, do které jsem nespadl:** working tree měl na startu necommitnutý revert display-ID → raw-DB-id v `remind_cli.py` (testy k němu neupravené → padaly by). Ověřením proti **serveru (zdroj pravdy přes `ssh`)** potvrzeno, že server i HEAD mají display-ID → raw-id byl stray edit, který agent správně zahodil. Bez ověření serveru bych mohl chybně „zachovat" rozbitý stav.
**Co zbývá:** Nasadit na server (`rsync` celého skillu) — server má pre-refactor display-ID verzi bez `store.py`; před přepisem porovnat se serverem (žádná autonomní divergence se nečeká, ověřeno). Skripty se čtou per-invocation → bez restartu. Případný `decisions.md` zápis (samostatný `store.py` vs. growth `db.py`) k odsouhlasení autorem.
**Jak vrátit zpět:** `git checkout HEAD -- skills/remind/scripts/remind_cli.py && rm skills/remind/scripts/store.py` (lokálně necommitnuto). Na serveru: `rsync` předchozí verze skillu zpět.
## 2026-06-15 05:50 — /remind: dokončení refaktoru (remind_send.py) + nasazení na server
**Cíl:** Dotáhnout volitelný krok — přesun read dotazů `remind_send.py` do `store.py` — a nasadit celý refaktor na server.
**Co jsem zkusil:** Do `store.py` přidány sender helpery (`due_at`, `enabled_cron`, `enabled_random`, `is_fire_delivered`, `record_fire`). `remind_send.py` přepsán: `_due_*` volají `store.*`, `main()` přes `store.connection`, žádné inline SQL ani `get_db`/`init_db`/`conn.close`. `record_fire` bez časové logiky (store je bezčasový) — `delivered_at` počítá `remind_send._record_fire` a předává explicitně. Nasazení: `rsync` server→`tmp/server-remind/` + diff proti lokálu — `db.py`/`forecast.py`/`random_times.py`/`SKILL.md` shodné (žádná autonomní divergence), lišil se jen refaktor → `rsync` lokál→server, owner `nanobot:nanobot` ověřen.
**Co fungovalo a proč:** 80/80 testů zelených (testy volají jen `main()`+patch `_send_telegram`/`_now_prague`/`DB_PATH`, vnitřní `_due_*` netestují přímo → přesun bezpečný). Server smoke: `list` (display-ID #1/#2/#3) + import `remind_send`+`store` OK. Cron sender (`* * * * * uv run .../remind_send.py`) vezme nový kód při příštím běhu — per-invocation read, bez restartu. **Lokálně necommitnuto** (kód skillu commituji jen na vyžádání).
**Jak vrátit zpět:** `git checkout HEAD -- skills/remind/scripts/remind_cli.py skills/remind/scripts/remind_send.py && rm skills/remind/scripts/store.py`, pak `rsync` HEAD verze skillu na server.
## 2026-06-15 13:21 — /remind: explicitní instrukce pro vypsání čtecích příkazů
**Cíl:** Uživatel hlásil, že po nedávných úpravách `/remind` dva různé modely „nedokážou vrátit celou tabulku" a každý to obchází jinak. Najít příčinu (nic neměnit), pak opravit instrukce ve skillu.
**Co jsem zkusil:** Reprodukce CLI lokálně i na serveru (`uv run … list`) → **exit 0, kompletní 18řádková tabulka**. Vyloučeno: pád, `uv`/PATH (uv jen v login shellu `~/.local/bin`, ale nanobot `exec` ho má), chybějící croniter (systémový python3 ho nemá — `from croniter import croniter` na ř. 22 → ImportError, ale agent jede přes uv), i refaktor `store.py` (`3de1d6b` sáhl jen na Python, `SKILL.md` se nedotkl). Stáhl jsem dvě webui session z poslední hodiny (`workspace/sessions/`): `552c62c9` (**kimi27**) i `ee623e4b` (default model) — **obě** spustily `list` úspěšně a dostaly celou tabulku, ale kimi27 odpověděl jen *„Máš 18 aktivních připomínek."* a druhý si postavil vlastní parafrázovanou markdown tabulku.
**Co fungovalo a proč:** Příčina = **mezera v `SKILL.md`, sekci `## Behavioral contract`**: popisuje *formát* výstupu `list` (uvozený jako ``` ukázka → vypadá jako schéma dat), ale nikde neříká „výstup předej uživateli kompletní". Modely to čtou jako surová data a každý si je naformátuje/zkrátí po svém. Fix: nový odstavec **Showing read results** na začátku sekce — `list`/`upcoming`/`delivered` vrací user-ready text, vypsat každou položku, nesumarizovat na počet, nevynechávat řádky; hezčí formát (tabulka, parafráze cronu) povolen, pokud zůstane `#display-id`. Plus oprava zastaralé argparse help hlášky `remind_cli.py` (`list` „as JSON" → „as readable text"). Nasazeno `rsync`em (owner `nanobot:nanobot` OK), CLI dál exit 0 / text, help aktualizován. Skilly se čtou per-tah bez restartu.
**Co zbývá:** Behaviorální ověření dělá uživatel ve webui — znovu `/remind list` s kimi27 i default modelem, ověřit že oba vypíšou všech 18 položek s `#display-id`. CLI test to neověří (chyba byla v chování modelu, ne ve skriptu).
**Jak vrátit zpět:** `git checkout HEAD -- skills/remind/SKILL.md skills/remind/scripts/remind_cli.py`, pak `rsync` předchozí verze skillu na server.
## 2026-06-15 13:28 — /remind: iterace formátu výpisu — kompaktní, model-side, jen enabled
**Cíl:** Po prvním kole (13:21) modely vypisovaly *všech* 18 položek, ale věrně relayovaly raw formát CLI → ukecané, víceřádkové, `[enabled]` u všech, nečitelné. Uživatel chce, ať to **model sám přepíše do pěkné kompaktní podoby**, vypíše **jen enabled** a `[enabled]` tag vynechá. **CLI se nemění — dělá to model.**
**Co jsem zkusil:** Přepsán odstavec **Showing read results** v `SKILL.md` (ř. 35): pro `list` model přepíše raw výstup do kompaktní podoby vlastní volby (řádek na připomínku, cron parafrázovaný do přirozené řeči), vypíše jen enabled (disabled přeskočí), zachová `#display-id` přesně jak je CLI vypsalo (mezery z přeskočených disabled OK — aby `--id` dál sedělo na `active_display_order`, které zahrnuje i disabled), bez `[enabled]` tagu. Obecné „nesumarizovat na počet" zůstává pro list/upcoming/delivered. Raw-formát blok pod tím ponechán (dokumentuje vstup, který model přepisuje). CLI `cmd_list`, `store.py` ani testy nedotčeny.
**Co fungovalo a proč:** Záměrně model-side render, ne změna Pythonu — uživatel chce „pěknou podobu podle sebe", což je prezentační volba, ne deterministický kontrakt. Klíčový constraint v instrukci: zachovat původní `#display-id` i při skrytí disabled, jinak by se rozbila vazba `--id` ↔ pořadí v CLI. Nasazeno `rsync`em (owner `nanobot:nanobot` OK), bez restartu.
**Co zbývá:** Behaviorální ověření dělá uživatel ve webui — `/remind list` s kimi27 i default modelem: kompaktní seznam, řádek/připomínka, bez `[enabled]`, jen enabled, se zachovanými `#display-id`.
**Jak vrátit zpět:** `git checkout HEAD -- skills/remind/SKILL.md` (vrátí na verzi z 13:21), pak `rsync` na server.
**Potvrzeno (13:3x):** Uživatel ve webui ověřil — výpis vypadá pěkně, kompaktní. Smyčka uzavřená.
## 2026-06-16 21:20 — llm-wiki: port pluginu jako skill (lokální PoC) + nasazení na server
**Cíl:** Portovat [llm-wiki-plugin](https://github.com/praneybehl/llm-wiki-plugin) (Karpathyho „LLM Wiki" pattern) jako nanobot skill — PoC, čistě lokálně, wiki v `workspace/cml/`, git nad `workspace/` = safety net. Plán: [plans/llm-wiki-skill.md](plans/llm-wiki-skill.md). (Širší multi-klient vize je odložená case study `plans/wiki-znalostni-baze.md`.)
**Co jsem zkusil:** Naklonoval plugin do `tmp/`. Zjištění proti WebFetch odhadu: **SKILL.md má jen 160 ř.** (ne 2200 — tělo už rozdělené, detaily v `references/`), 7 stdlib skriptů (PyYAML jen `wiki_graph_lint`+`wiki_graph_extract`, `wiki_graph_query` čte sqlite). Zkopíroval `SKILL.md`+`scripts/`+`references/`+`assets/` (assets nutné — `init_wiki.py` z nich kopíruje šablony) do `skills/llm-wiki/`; vynechal `commands/` (CC-only slash, duplikují SKILL.md), `.claude-plugin/`, `integrations/`. Skripty: PEP 723 hlavička (`uv run --script`, deps `[]` / `["pyyaml"]`); default cesty `Path("wiki")`→`Path("cml/wiki")` u 4 skriptů s defaultem (robustní — agent nemusí cestu pamatovat). Docstringy skriptů nechal generické (skript je generický, nanobot-ismus patří do SKILL.md). SKILL.md: sed `python …scripts/`→`uv run skills/llm-wiki/scripts/`, `.py wiki/`→`.py cml/wiki/`; přidal **„Nanobot adaptation"** sekci (fixní `cml/wiki`+`cml/raw`, uv run, separate store mimo note/keep/MEMORY, **Dream nesahá do cml/**, **lint report-only**, jazyk uživatele, local PoC); krok agent-memory-integration vypnut. Bootstrap: `init_wiki.py . --wiki-dir cml/wiki --raw-dir cml/raw` (root `.` existuje, init `<project-root>` sám nevytváří).
**Co fungovalo a proč:** Lokální smoke (`tmp/wiki-test`): init→2 vzorové stránky→lint (detekoval broken `[[wikilink]]`)/stats/search (BM25)/graph_lint (validace ontologie)/graph_extract (2 nodes/3 edges, 0,15 s)/graph_query (neighbors prochází `mentions`+typed edge) — vše přes `uv run` s defaultem `cml/wiki` bez předání cesty. Server: `rsync` skillu (owner `nanobot:nanobot` ✓), git nad `workspace/` potvrzen, `uv` v `~/.local/bin/uv` (mimo non-login PATH — pro SSH/cron plná cesta, gotcha #8). Init na serveru vytvořil `cml/wiki`+`cml/raw` (`nanobot:nanobot`), lint/stats/graph_extract běží pod serverovým `uv` (PyYAML stažen, 0,26 s). Skill se čte per-tah bez restartu. **Pattern sedí na nanobot 1:1** — `edit_file`=chirurgické `str_replace`, exec=python, nezávislost na CC hooks/MCP potvrzena.
**Co zbývá:** (1) Behaviorální E2E přes Telegram/WebUI — NL trigger („ingest this into my wiki…", „what does my wiki say about X") dělá uživatel; ověřit reálný ingest (chunkování velkého zdroje vs 60 s exec limit, #5). (2) **Fáze B:** periodický `wiki_lint` přes HEARTBEAT (report-only). (3) Kód skillu lokálně **necommitnuto** (commituji jen na vyžádání).
**Jak vrátit zpět:** Lokálně `rm -rf skills/llm-wiki`. Server: `ssh nanobot@nanobot.hell 'rm -rf ~/.nanobot/workspace/skills/llm-wiki ~/.nanobot/workspace/cml'`.
## 2026-06-16 22:15 — llm-wiki: rychlý capture + dávkový compile (systémový cron → wiki_compile.py)
**Cíl:** PoC ingest přes WebUI trval **1:41** (agentní compile, ne skripty) → nepoužitelný interaktivně. Oddělit **capture** (instantní hod zdroje do `cml/raw/`) od **compile** (těžký raw→wiki) a compile spouštět na pozadí přes systémový cron. Plán: [plans/llm-wiki-skill.md](plans/llm-wiki-skill.md), sekce „Capture + dávkový compile".
**Co jsem zkusil:** Nový `skills/llm-wiki/scripts/wiki_compile.py` (vzor `detach/tasks-daemon.py`): shebang `uv run --script` + PEP 723 `["nanobot-ai"]`, ale **`from nanobot import Nanobot` odložen do `run_compile()`** — levná per-minutová pre-kontrola nesmí platit import těžkého balíku. Tok: (1) scan `cml/raw/` na pending regulérní soubory (mimo `_done/`/`_hard/`/`assets/`) → prázdno = exit 0 bez LLM; (2) atomický lockfile `cml/.compile.lock` (PID+timestamp) přes `os.open(O_CREAT|O_EXCL)`, stale-detekce (mrtvý PID přes `os.kill(pid,0)` / stáří > 30 min se přebere); (3) `Nanobot.from_config()` + `asyncio.wait_for(bot.run(drain_goal, session_key="wiki-compile"), 25min)` — drain = všechno nasbírané v jednom dávkovém běhu; (4) tiše, jen log do `log/wiki_compile_cron.log`. SKILL.md: nová sekce **„Capture vs compile"** (capture zapíše `cml/raw/<slug>.md` + STOP, escape hatch „compile now"; `_done/`/`_hard/` konvence) + ingest workflow zarámován jako compile/drain + **fix slug-kolize** (concept/entity = krátký název pojmu, source = slug zdroje — řeší původní kolizi RoPE). Crontab `nanobot` (PATH= už nastaven kvůli remind): `* * * * * uv run …/wiki_compile.py >> …/wiki_compile_cron.log 2>&1` (forma `uv run <path>` jako remind, **ne** holý shebang — non-login/cron PATH nemá uv → `env: uv: No such file or directory`, gotcha #8).
**Co fungovalo a proč:** Lokálně (mock workspace v `tmp/`, `--dry-run`, čistý stdlib bez uv) ověřeny všechny větve: prázdný inbox→exit 0 bez logu; pending→dry-run+lock uvolněn; živý lock (reálný PID)→SKIP, lock zachován; stale lock (mrtvý PID)→reclaim. Server: deploy `rsync` (owner ✓), **reálný compile** leftover `rotary-position-embedding.md` proběhl na pozadí za 100 s, `remaining=0`, přesun do `_done/`, vznikly `concepts/rope.md` + `sources/rotary-position-embedding.md` + `entities/paper-rope.md` (slug fix drží — žádná kolize), index+log+graph (nodes/edges/sqlite/graphml) regenerovány. **Empty-inbox fast path: 0,119 s, exit 0, žádné volání LLM** — to je jádro efektivity minutového pollingu. **Živý cron test:** dropnut `alibi-position.md` → cron ho zvedl za ~50 s (hranice minuty), compile 107 s, přesun do `_done/`, `concepts/alibi.md`+`entities/paper-alibi.md`+`sources/alibi-position.md`, cross-link `[[rope]]`. Celý pipeline (capture instant → cron compile dávkově) běží end-to-end.
**Co zbývá:** (1) Capture přes WebUI/Telegram ověří uživatel — „ulož X" musí vrátit **instant ack** bez 1:41 (kód capture je instrukce v SKILL.md, ne skript → behaviorální). (2) Drobnost: agent v `log.md` napsal concept jako `concepts/rotary-position-embedding.md`, reálný soubor je `concepts/rope.md` (kosmetické, `[[rope]]` v indexu sedí). (3) Kód skillu lokálně **necommitnuto** (commituji jen na vyžádání). (4) Fáze B periodický lint přes HEARTBEAT — stále odloženo.
**Jak vrátit zpět:** Crontab: `ssh nanobot@nanobot.hell 'crontab -l | grep -v wiki_compile | crontab -'` (smaž i komentář řádek). Skill: `git checkout HEAD -- skills/llm-wiki/SKILL.md && rm skills/llm-wiki/scripts/wiki_compile.py`, pak `rsync` na server. Test artefakty (PoC demo obsah, lze nechat): pages pro rope/alibi v `cml/wiki/` + `cml/raw/_done/*` + `log/wiki_compile_*.log`.
## 2026-06-16 22:45 — llm-wiki: zpřísnění capture brány (capture/compile split se nedodržoval)
**Cíl:** Ověřit capture chování (uživatel chtěl testovat přes WebUI). Test odhalil regresi: capture/compile split se nedodržuje.
**Co jsem zkusil:** Pustil capture-style goal přes `bot.run(session_key="wiki-capture-test")` (transport-nezávislé, jako WebUI tah): „Ulož mi do wiki: <YaRN fakt>". **Agent ignoroval capture instrukci a udělal celý inline compile za 233 s** — vytvořil concept/entity/source stránky, upravil rope.md, rozšířil ontologii, překompiloval graf. Tedy přesně to, čemu jsme se chtěli vyhnout (původně 1:41, teď ještě hůř). Příčina: tělo SKILL.md je převážně návod „jak udělat plný ingest", a model „ulož do wiki" čte jako „proveď ten workflow"; moje sekce „Capture vs compile" to nepřebila — byla příliš měkká a tonula v okolním textu. **Druhý problém:** inline compile nepřesunul zdroj do `_done/`, takže cron pak zvedl `cml/raw/yarn.md` znovu a drain agent se **zacyklil** (132 iterací) reconciliací už existujících stránek. Runaway proces (PID 320950, ~6 min) jsem `pkill -TERM`-l, smazal lock, přesunul `yarn.md` → `_done/` (zastavilo příští cron tik). Fix SKILL.md: (1) **tvrdé pravidlo nahoře** v „read this first" — interaktivní požadavek = CAPTURE ONLY, inline compile je bug; (2) capture bullet přepsán na 3 kroky (zápis do `cml/raw/`, jednořádkový ack, STOP) + **výčet zakázaných akcí** (žádné čtení SCHEMA/index, žádné stránky, žádné skripty, žádný graf); (3) escape hatch zúžen („add to wiki" NENÍ escape); (4) STOP-redirect na začátku ingest workflow sekce. `wiki_compile.py` drain goal dostal **idempotenci** (existující stránky → `_done/`, necyklit; vždy přesunout z `cml/raw/`).
**Co fungovalo a proč:** Po redeploy znovu-test (Flash Attention): **capture 26 s** (jen cold-start `bot.run`, ne compile), odpověď „Zachyceno — zkompiluju na pozadí.", vznikl jen `cml/raw/flash-attention.md`, **žádná wiki stránka inline** — brána drží. Cron pak flash-attention zkompiloval na pozadí čistě (188 s, `remaining=0`, přesun do `_done/`, 3 stránky, **bez loopu** — idempotence + nový zdroj). Skill commitnut (`751a11a` celý skill na výslovnou žádost; `9bc52d8` zpřísnění brány).
**Co zbývá:** (1) Uživatel otestuje capture/query/lint sám přes WebUI (teplá session → svižnější než 26 s). (2) **Drobnost — slug kolize u Flash Attention:** source i concept dostaly slug `flash-attention` (u RoPE se přirozeně lišily `rope`/`rotary-position-embedding`); pravidlo „při kolizi suffixuj" agent neaplikoval → `[[flash-attention]]` dvojznačný mezi `sources/`+`concepts/`. Demo obsah, ne pipeline bug. (3) **Compile time roste s velikostí wiki** (100→188 s) — agent čte víc kontextu; na pozadí nevadí latencí, ale roste token cost. (4) Fáze B periodický lint přes HEARTBEAT — odloženo.
**Jak vrátit zpět:** `git checkout HEAD~2 -- skills/llm-wiki/SKILL.md skills/llm-wiki/scripts/wiki_compile.py` (před zpřísněním), `rsync` na server. Runaway compile už zabit, lock čistý, `cml/raw/` prázdné (vše v `_done/`).
## 2026-07-01 10:40 — note: kompletní přepis na capture → compile pipeline (nasazeno)
**Cíl:** Překopat `/note` z SQLite row-store (add/list/show/delete/tagy/display ID) na osobní znalostní bázi: dumb capture z různých kanálů → periodický compile do JEDNOHO strukturovaného `notes/notes.md` s tematickými sekcemi; hledání = načíst celý dokument. Bez DB, čistě detach-style adresáře. Plán: [plans/note-prepis.md](plans/note-prepis.md).
**Návrh (brainstorming + serverová verifikace):** Architektonicky stejný vzor jako `/llm-wiki`, ale záměrně lehčí (jeden doc, žádný index/graph/lint). Rozhodnutí: oddělený lehký skill; tematické sekce řídí LLM; **dual-mode** `/note <text>` = okamžitě inline compile (default), `/note cron <text>` = odloženě přes cron; tagy zrušeny; mazání = editace dokumentu. Ověřeno na serveru před psaním kódu: **`web` tool JE zapnutý** (config `tools.web`: DuckDuckGo + Jina Reader fetch — `TOOLS.md` ho nezmiňuje!) → URL fetch přes něj, ne curl; crontab uživatele `nanobot` má `PATH=` hlavičku → bare `uv run` funguje; `notes/` existoval prázdný; žádný drift skillu; `note.sqlite` = 17 aktivních notes.
**Co jsem udělal:**
- `note_capture.py` — dumb atomic capture (timestamp `%Y-%m-%d_%H_%M_%S_%f`, staging→`os.replace`, frontmatter provenience, ASCII-fold slug pro diakritiku) → `notes/inbox/` + řádek do `log/note.log`.
- `note_compile.py` — tenký launcher (kostra z `wiki_compile.py`): fs pre-check bez importu nanobota, lockfile se stale-reclaim, `Nanobot.from_config().run(DRAIN_GOAL)` (process_direct, **bez cron preamble** — proto systémový crontab, ne jobs.json), timeout 15 min. DRAIN_GOAL jmenuje skill → headless agent načte SKILL.md.
- `SKILL.md` přepsán (EN, dual-mode, sdílený compile workflow, URL přes `web` tool + paywall→`hard/`, search = celý `notes.md`). Description zpřesněn proti kolizi s `keep`.
- Lokální pytest (16, zelené). Starý `note.py` odstraněn z repa. Commity: `a8f1997` (plán), `afe05d8` (skill), `02f9e7a` (description/search fix).
- Nasazeno `rsync`, crontab řádek přidán (`* * * * * uv run …/note_compile.py`, každou minutu jako wiki/remind). Migrace: 17 aktivních notes → inbox seeding → první drain postavil `notes.md`. Starý `note.sqlite` → `backup/note-retired/`; stray `notes.db`/`note.py`/`__pycache__` smazány.
**Co fungovalo a proč:** První manuální drain 17 notes = **45 s**, `notes.md` s čistými sekcemi (DevOps, Chata, DT Glass, Work/ZOT…); **URL reálně stažena** (Jina Reader) — DT Glass expandováno na produkty s cenami/rozměry, obě URL bare na vlastních řádcích, žádná fabrikace. E2e přes reálného agenta: cron **auto-fire** (deferred zachyt zpracován do ~80 s), `/note <text>` **immediate** (routing → note skill, capture + inline compile → nová sekce), `/note search` (načte `notes.md`, odpoví). Vše zelené.
**Co zbývá / poznámky:** (1) **Kolize triggerů `note` vs `keep`:** NL „poznamenej si …" původně spadlo do `keep` (obě claimovaly „ulož si"). Fix: description zpřesněn (odstraněno „ulož si", přidán kontrast „NOT durable facts → keep"). Spolehlivý je explicitní `/note` prefix (ten routuje správně — ověřeno). Uživatel ať v Telegramu používá `/note …` / `/note cron …`. (2) Paywall detekce je prompt-driven, live paywall netestován (logika ověřena návrhem). (3) Compile běží na default presetu; volitelně jde přepnout na rychlejší. (4) Immediate režim blokuje tah 15100 s a roste s velikostí `notes.md` — proto `/note cron` pro dávkové syp­ání.
**Jak vrátit zpět:** Repo: `git revert 02f9e7a afe05d8` (nebo `git checkout afe05d8~1 -- skills/note/`). Server: `git`-verzi note skillu `rsync`-ni zpět + obnov data `mv ~/.nanobot/workspace/backup/note-retired/note.sqlite ~/.nanobot/workspace/db/`. Crontab: `crontab -l | grep -v note_compile | crontab -` (smaž i komentář). `notes/` (notes.md, inbox/done/hard) lze smazat — původní data jsou v `backup/note-retired/note.sqlite`.
## 2026-07-01 11:35 — note: mazání s odsouhlasením + git commit `notes.md` po každé změně
**Cíl:** Doplnit do přepsaného `/note` (viz 10:40) dvě chybějící provozní věci, na které uživatel upozornil: (1) **mazání poznámek** — s tvrdým pravidlem, že model před smazáním vždy ukáže přesný text a čeká na odsouhlasení; (2) **verzování `notes.md`** — po každé změně `git add notes/` + `git commit`. Plán: `~/.claude/plans/u-nove-vytvoreneho-skillu-declarative-unicorn.md`.
**Klíčové zjištění (proč to bylo potřeba):** Workspace `~/.nanobot/workspace/` **je** git repo, ale commituje ho **jen Dream** (`dream: periodic memory consolidation`) a ten se `notes/` dotýkat nesmí. Ověřeno na serveru: celé `notes/` bylo **untracked** (`?? notes/`) → `notes.md` se nikdy necommitovalo. Žádný existující skill vlastní commit nedělá → vzor nový.
**Rozhodnutí (dotazy na uživatele):** Mazání = **přirozeným popisem** (žádná display ID / `/note list` — sedí na prózový návrh). Před smazáním **hard-gate**: ukázat verbatim text + čekat na „ano". Commit = `git add notes/` + `git commit` (verzuje se celé `notes/`, jen `.compile.lock` do `.gitignore`). **Jeden committer per cesta, žádné dvojité commity:** inline `/note <text>` a mazání → agent přes `exec`; cron drain → **deterministicky v Pythonu** v `note_compile.py`. Sdílený *Compile workflow* zůstává bez commitu (commit je odpovědnost volajícího). Prefix `note:` (konzistentní s `dream:`).
**Co jsem udělal:**
- `SKILL.md`: do `description` přidány delete trigger fráze (`smaž poznámku o X`, `zapomeň X`, `uprav poznámku o X`); do `/note <text>` krok commit přes `exec`; sekce `Delete / edit` přepsána na tvrdý dvoukrok (najdi → **ukaž přesný text + čekej na potvrzení** → surgical `str_replace` + commit; totéž pro edit; pending zachyt = smaž soubor v `inbox/`); nová sekce `Versioning (git)`.
- `note_compile.py`: `commit_notes(count)` — po úspěšném drainu (za drženého locku, `.compile.lock` je gitignored → nestaguje se) `git status --porcelain notes/` → prázdné = no-op; jinak `git add notes/` + `git commit -m "note: cron drain (N captures)"`. Nikdy `git add -A` (Dream vlastní zbytek). Chyba commitu se jen loguje (drain už uspěl, nesmí se hlásit jako fail).
- Testy `test_note_compile.py`: +3 (`commit_notes` vytvoří commit / no-op při čistém stavu / respektuje gitignored lock), fixture dostal `WORKSPACE`. **19 zelených** lokálně.
- Server `.gitignore`: přidán `notes/.compile.lock` **před** deployem (živý pid-lock držený během commitu do gitu nepatří) — commit `ad3337f`.
- Deploy `rsync` (`--exclude __pycache__`), owner `nanobot:nanobot` ✓.
**Co fungovalo a proč:** **E2E cron cesta ověřena na reálné infře:** seed testovacího zachytu (unikátní marker `ZZZTESTVERIFY…`, přímo do `inbox/` — ne přes capture, aby nešpinil `note.log`) → cron tik do ~80 s → agent zařadil do `notes.md` pod sekci, launcher udělal commit `note: cron drain (1 captures)`, `git status notes/` **čistý** (`.compile.lock` se nepletl → gitignore OK), cron log `END … COMMIT notes/ (1 captures)`. Tento první commit navíc **bootstrapnul celý dosud untracked `notes/` strom** (notes.md + 18 done souborů) — přesně očekávané. `git` je `/usr/bin/git` v2.47.3, `tools.exec` má prázdné `allowPatterns`/`denyPatterns` (git neblokovaný) + reálný workspace commit prošel → exec-git i launcher jsou spolehlivé. Úklid **forward** (bez přepisu historie): odstraněn testovací řádek + done soubor, commit `chore: remove note commit verification test` — marker pryč, `## Nanobot` sekce nedotčená. Serverové commity: `ad3337f` (.gitignore), `cffe3b3` (drain — bootstrap notes/), `5ed6832` (cleanup).
**Co zbývá:** Inline `/note <text>` commit přes `exec` a **mazání s odsouhlasením** nebyly spuštěny živě (LLM-driven; mazání navíc čeká na interaktivní potvrzení → nejde smysluplně headless). Vysoká jistota ze statického ověření (git dostupný, exec bez denylistu, workspace commit funguje) — dořeší reálné použití přes Telegram (`/note …` / „smaž poznámku o …"). Poznámka: `git add notes/` verzuje i `done/` → historie poroste (dle přání uživatele; případné prořezání mimo rozsah).
## 2026-07-01 — note/keep/bookmark: zpřesnění description o vzájemné hranice (routing)
**Cíl:** Dispatcher nanobota má jednoznačně poznat, kam vstup patří — `note`, `keep` a `bookmark` mají překrývající se doménu (poznámka vs. fakt vs. URL na přečtení). Vykonání schváleného plánu: doplnit do `description` všech tří skillů explicitní hranice vůči sousedům.
**Co jsem zkusil:** Server byl před editací ověřen jako shodný s lokálem (žádný Dream zásah), takže editace šla přímo do lokálního repa bez tahání ze serveru. Tři exact-string úpravy `description`:
- `skills/keep/SKILL.md` — nově explicitně „durable personal facts, preferences, and decisions" + „NOT collecting notes, links, or articles (that is note)".
- `skills/note/SKILL.md` — poslední dva řádky přeformulovány na „reference material you file and later query" + „NOT durable personal facts, preferences, or decisions (that is keep), and NOT a bare URL you just want to read later (that is bookmark)".
- `skills/bookmark/SKILL.md` — doplněno „NOT filing or summarizing content into a knowledge base (that is note)".
**Co fungovalo a proč:** První pokus o editaci `note/SKILL.md` selhal (`old_string` s pouze poslední dvěma řádky nebyl v souboru nalezen), oprava — rozšířil jsem `old_string` o předchozí kontextový řádek, čímž byl znovu jednoznačný, a editace prošla; obsah po opravě odpovídal plánu přesně. Nasazeno `rsync -av` (celé adresáře, ne jen `SKILL.md`) na server přes IP `192.168.4.64` (hostname `nanobot.hell` se z tohoto stroje nerozeznává) pro všechny tři skilly. Vlastník na serveru ověřen `ls -l` — všechny tři `SKILL.md` `nanobot:nanobot`. Zpětné stažení serverových `SKILL.md` a `diff` proti lokálu — pro všechny tři soubory prázdný diff (plná shoda). Skilly se čtou při každém tahu bez restartu služby, takže zpřesnění je live okamžitě.
**Co zbývá:** Nic — nasazeno a ověřeno. Behaviorální potvrzení (že dispatcher skutečně routuje lépe na hraničních vstupech) přijde z reálného použití přes Telegram/WebUI.
**Jak vrátit zpět:** Repo: `git checkout HEAD~1 -- skills/note/` (vrátí SKILL.md + launcher + testy). Server: `rsync` starší verzi skillu zpět; `git.gitignore` řádek `notes/.compile.lock` odstranit + `git revert ad3337f`. Feature je aditivní — necommitování `notes.md` je návrat k původnímu chování (Dream se `notes/` stejně nedotýká).
## 2026-07-01 12:00 — note: bug při `/note delete` (hledal v SQL DB, neptal se, mazal done) → hardening SKILL.md
**Cíl:** Uživatel po nasazení mazání hlásil, že `/note delete zot dokumentace` (a) se na nic nezeptalo, (b) smazalo i soubor v `done/`, (c) agent se to snažil hledat v SQL DB. Najít příčinu (systematic-debugging), pak opravit.
**Co jsem zjistil (z reálné stopy `sessions/websocket_*.jsonl`):** Přesná sekvence tahu: agent paralelně `read_file SKILL.md` **a zároveň** `grep "zot" v db/` → `exec sqlite3 db/notes.sqlite "SELECT … FROM notes WHERE …"` (tím **vytvořil prázdný `db/notes.sqlite`**) → `.tables` → `find_files db` → teprve pak `grep *.md` → našel v `notes.md` + `done/…zot.md` → `edit_file` + `rm done` **bez potvrzení**. Reasoning: *„Let me check the notes database more carefully."* **Příčiny:** (1) model má silný prior „`/note delete` = smaž řádek v notes DB" (chování STARÉHO SQLite note skillu) + `AGENTS.md` konvence „SQLite do `db/*.sqlite`"; (2) `SKILL.md` **nikde nezakazoval** DB/sqlite a **nedokumentoval `/note delete`/`/note edit` jako příkazy** → model spadl do starého vzoru; (3) potvrzovací brána byla jen „prozaická", ne tvrdá STOP jako u `/note cron`. **NENÍ to stará paměť:** `MEMORY.md` note nezmiňuje, `history.jsonl` (kde stará „note=sqlite" lekce je, ř. 344) agent per-tah nečte (jen Dream) — je to prior modelu + slabý model (kimi/glm třída).
**Fix (SKILL.md hardening, per rozhodnutí uživatele „done/ nechat jako interní"):**
- **Anti-DB callout** v Architecture: „No database — ever … never search `db/`, never run `sqlite3`, never create/open `.sqlite`/`.db`; AGENTS.md SQLite konvence se na notes nevztahuje; starší verze DB měla, je pryč." Přímo proti krokům ze stopy.
- **Delete/edit přepsán** na `## /note delete <query> / /note edit <query>` (mapování na příkaz, který uživatel reálně napsal) + **tvrdá dvou-tahová STOP brána** (vzor z `/note cron`, který prokazatelně funguje): Turn 1 = najdi **jen v `notes.md`** (zákaz db/sqlite/inbox/done/hard) + ukaž verbatim text + **STOP the turn** (zakázáno `str_replace`/`rm`/`git`); Turn 2 = teprve po explicitním „ano" smaž + commit. **Mazání se dotýká jen `notes.md`, NE `done/`** (breadcrumb je interní plumbing).
- Úklid: smazán prázdný `db/notes.sqlite` (0 B, artefakt chybného SQL pokusu) — jeho existence agenta utvrzovala v mylné víře (`find_files db` → notes.sqlite).
**Co fungovalo:** Deploy `rsync` (jen SKILL.md), owner `nanobot:nanobot` ✓, oba fixy v nasazeném souboru ověřeny grepem. Skill se čte per-tah bez restartu → příští `/note delete` běží na opravené verzi.
**Co zbývá:** Behaviorální potvrzení dělá uživatel (příští `/note delete` musí: hledat jen v `notes.md`, ukázat text, zastavit a čekat, nesahat na `done/`). U slabšího modelu STOP brána riziko silně snižuje, ale negarantuje 100 %. Poznámka: smazaná poznámka „ZOT dokumentace" byla reálná (ne testovací) — jde obnovit z gitu (`git show cffe3b3:notes/notes.md`), nabídnuto uživateli.
**Jak vrátit zpět:** Repo `git checkout HEAD~1 -- skills/note/SKILL.md`, pak `rsync` na server. (Aditivní/behaviorální změna — revert jen oslabí brány, nerozbije data.)
## 2026-07-01 12:15 — note: čeština v SKILL.md + zbytečné `/note` varianty v description
**Cíl:** Uživatel v IDE otevřel `skills/note/SKILL.md` a upozornil na dvě věci: (1) v souboru je čeština, ačkoli EN-only pravidlo je explicitní v projektovém `CLAUDE.md`; (2) proč description obsahuje `/note X`/`/note cron X` varianty, když nanobot ví, jak skill spustit. Požádal i o celkovou kontrolu kvality promptu.
**Co jsem zjistil:** Čeština byla na 4 místech (description trigger fráze + 3 místa v těle — cron potvrzení, delete/edit triggery, potvrzovací slova Turn 2). Pro otázku 2 jsem našel přímý podklad v `knowledge.md` („Skill description — k čemu reálně slouží"): description je čistě routing signál (kdy/proč, ne jak) a viditelný jako jeden řádek `- **note** — <description>`, kde jméno skillu je vždy vidět — `/note X` a `note X` tedy nic nerozlišují nad rámec samotného jména, a `/note cron X` je navíc detail subakce („jak"), ne routing info. Sesterský `bookmark` (upravovaný ve stejném committu jako `note`) tento princip u slash-forem už dodržuje.
**Co fungovalo a proč:** Oprava description (odstraněny CZ fráze + `/note X`/`/note cron X`, zachována jen anglická disambiguace od `keep`/`bookmark`) + 3 CZ místa v těle převedena na anglické ekvivalenty + doplněn jazykový tag ```text``` k fencovanému stromu adresářů (chybějící dle `markdown-style.md`). Uživatel si mezitím sám upravil první větu description na „Capture notes, texts, ideas…" — necháno beze zásahu. Vedlejší nález: `fix-markdown.ps1` hook po Edit tahu měnil emphasis z hvězdiček na podtržítka (`*only*`→`_only_`, `*Versioning*`→`_Versioning_`) — v rozporu s `markdown-style.md` („Never underscores"); opraveno zpět, hook to už neměnil zpátky. Nasazeno na server: hostname `nanobot.hell` se z tohoto stroje nerozeznává (`Could not resolve hostname`), IP `192.168.4.64` z `CLAUDE.md` funguje. Server byl před přepisem ověřen jako identický s předchozím lokálem (žádný Dream zásah) — `rsync -av` bezpečný. Po nasazení `diff` server↔lokál prázdný, owner `nanobot:nanobot` ✓.
**Co zbývá:** Stejný „zbytečná /command varianta" vzor mají i `keep` (`/keep X`), `remind` (`/remind`) a `plan` (`/plan X`) — mimo scope tohoto zadání (uživatel se ptal jen na `note`), zmíněno uživateli jako volitelný follow-up, nikam nezapsáno jako todo (čeká na rozhodnutí, jestli o to stojí).
**Jak vrátit zpět:** Repo `git checkout HEAD~1 -- skills/note/SKILL.md`, pak `rsync` na server (IP `192.168.4.64`).
## 2026-07-01 19:33 — note: doplnění diakritiky při reformulaci + echo přesného uloženého textu
**Cíl:** Uživatel chtěl dvě vylepšení `/note`: (1) když napíše text bez diakritiky, skill má diakritiku při reformulaci doplnit; (2) u okamžitého režimu (`/note <text>`) má potvrzovací zpráva ukázat přesný zapsaný text, ne jen název sekce. Přes plan mode (Explore + diff proti serveru) potvrzeno, že jde jen o immediate mode — `/note cron` v daném tahu nic nereformuluje, takže echo tam nedává smysl.
**Blocker a jeho vyřešení:** SSH na `nanobot.hell` (pod `nanobot` i `root`, stejný klíč) nejdřív padalo na `Permission denied (publickey,password)` a lokálně chyběl `rsync` — sandbox tohoto sezení neměl potřebný klíč/nástroj. Uživatel obojí mimo tuto session doplnil („uz funguje, opravil jsem to" / „uz jsem dodal i rsync"), pak SSH i rsync fungovaly bez úprav z naší strany.
**Co fungovalo a proč:** V `skills/note/SKILL.md` (1) bod „Reformulate" v Compile workflow rozšířen o instrukci: text bez diakritiky (příklad „kdyz uz to psal bez hacku") se při reformulaci opraví na spisovnou diakritiku, už-diakritizovaný a ne-český text se nechá být; (2) krok 5 v `/note <text>` nahrazen tak, aby kromě sekce citoval i doslovný zapsaný text (verbatim, ne převyprávění). Před přepisem staženo `rsync` ze serveru a porovnáno s lokálem — server měl starší, ale skriptově identickou verzi (chyběly jen předchozí neasazené kosmetické commity popisu, žádný Dream zásah) → bezpečné nahradit celý adresář. Nasazeno `rsync -av skills/note/` (celý adresář, vč. `scripts/`, `tests/`), owner `nanobot:nanobot` ověřen.
**Co zbývá:** Ruční test v chatu — `/note psal jsem bez hacku a carek` (diakritika v `notes.md`) a `/note <krátký text>` (potvrzení obsahuje přesný zapsaný text). Po ověření přesunout z `todo.md`/nezapsáno jako todo, protože šlo o přímé zadání bez čekání ve frontě.
## 2026-07-02 15:25 — bookmark: archivace plného textu článku jako čistý markdown
**Cíl:** Uživatel se zeptal, co skill `bookmark` udělá, když vloží URL a k tomu celý zkopírovaný článek (Ctrl+A/Ctrl+C). Zjištěno, že skill neměl žádné pravidlo — celý neočištěný článek by teoreticky přistál do `description` (`TEXT` bez limitu) a rozbil formát výpisů. Zadání: skill má rozpoznat plný článek, vyčistit ho (navigace, menu, reklamy, patičky), převést na markdown a uložit; `description` si dogenerovat sám, pokud ho uživatel neuvede.
**Klíčové zjištění (přes plan mode, 2× Explore agent):** Novou pip závislost (markdownify/trafilatura) na server **nedostanu** — bwrap sandbox na bare-metal `nanobot.hell` nemá přístup k uv cache/data adresářům (`knowledge.md` ř. 615634), balíčky by při stahování padaly. Proto **čištění a převod do markdownu dělá agent (LLM)** na úrovni instrukcí v `SKILL.md`, skript zůstává **stdlib-only**. Precedent pro echo uloženého textu už existuje v `/note` (commit 739b9c8) — přejat.
**Co fungovalo a proč:**
- `bookmark.py`: schéma rozšířeno o nullable `content TEXT` + idempotentní migrace v `_init_db` (`PRAGMA table_info` → `ALTER TABLE ADD COLUMN`, protože `CREATE IF NOT EXISTS` sloupec do existující DB nepřidá). `add --content-file <path>` (`-` = stdin, agent volá přes heredoc `<<'ARTICLE'` — jedno atomické volání, žádné shell-escapování). Nový `content <id>` vypíše uložený markdown. 📄 marker v `list`/`history`/`show` u bookmarků s obsahem (SELECT tahá `content IS NOT NULL AS has_content`, ne celý blob).
- Migrace ověřena na **kopii reálné serverové DB** (11 řádků) před nasazením — všech 11 řádků zachováno, `content` přidán jako NULL. ALTER ADD COLUMN je nedestruktivní.
- Testy `skills/bookmark/tests/test_bookmark.py` (6×, vzor z `note`): migrace legacy DB, add s/bez obsahu, `content` příkaz, 📄 marker. Všechny zelené.
- `SKILL.md`: nová sekce „Saving an article's full text" (detekce + čištění + heredoc + echo verbatim), pravidlo pro URL bez textu (zkus `web`/Jina Reader, ale paywall/nesmysl NEUKLÁDAT, `⚠ paywall/incomplete`, nefabrikovat), `content` příkaz, marker v Output formatting, rozšířený Workflow.
- Před zásahem dotažena serverová drift úprava frontmatteru (`NOT ... (that is note)`) do repa (samostatný commit) — server byl zdroj pravdy.
- Nasazeno `rsync -av skills/bookmark/`, owner `nanobot:nanobot` ✓ (omylem nahrané `__pycache__` uklizeny na serveru i lokálně). End-to-end na serveru přes heredoc: obsah uložen, 📄 naskočil, `content 1` vypsal přesný markdown vč. zachované diakritiky. Testovací řádek pak smazán z reálné DB přes `sqlite3` (skill nemá delete příkaz) → zpět na 11 řádků.
**Co zbývá:** Behaviorální ověření dělá uživatel v chatu — vložit URL + článek a zkontrolovat, že agent text vyčistí, uloží a odcituje verbatim úsek; otestovat i URL-only (fetch) a paywall (neuloží obsah). Známé omezení: `content <id>` čte jen z **unread** setu (jako `show`) — obsah už přečtených bookmarků z `history` zatím nedostupný.
**Jak vrátit zpět:** Repo `git checkout HEAD~1 -- skills/bookmark/` + `git rm skills/bookmark/tests/`, pak `rsync` na server. Migrace DB je aditivní a nedestruktivní — `content` sloupec může na serveru zůstat (ignoruje ho starší verze skriptu). Data v `content` by revert skriptu zneviditelnil, ne smazal.
## 2026-07-02 17:00 — bookmark: soft delete, HTML→markdown utilita (trafilatura), fetch-fail výzva
**Cíl:** Tři navazující požadavky na `bookmark` (přes plan mode, opakované AskUserQuestion): (A) když se nepodaří stáhnout obsah URL, agent si má o vložení článku sám říct; (B) delete pro mazání článků (omylem vložený duplikát); (C) když je vložený obsah HTML, převést ho na markdown Python utilitou, ať to nedělá drahý model.
**Klíčová oprava předpokladu:** U hlavní feature (viz záznam 15:25) jsem tvrdil, že bwrap sandbox nepustí pip závislost. **Ověřeno naživo — na `nanobot.hell` to neplatí:** produkční `exec` běží `"sandbox": ""` (bez bwrap), `uv` + PyPI fungují, `uv run --with trafilatura` stáhne a naimportuje (v2.1.0). Jiné skilly reálně jedou se závislostmi (remind→croniter, llm-wiki→pyyaml). Bwrap limit z `knowledge.md` ř. 615634 platí jen pro nasazení se zapnutým sandboxem. → doplněna korekce do `knowledge.md`.
**Rozhodnutí uživatele (AskUserQuestion):** delete = **jen soft delete** (flag `deleted_at`, skryje z výpisů, řádek zůstává v DB; **bez** restore/deleted příkazů) + **potvrzení před smazáním** (řeší agent). HTML převod: uživatel navrhl markitdown; ověřeno ze zdrojáku, že markitdown pro HTML = markdownify (převede celé `<body>`, **neodstraní boilerplate**) + těžší (magika/onnxruntime) → dominovaná. Zvolena **trafilatura** (odstraní boilerplate A vrátí markdown v jednom kroku). Plain text: model čistí **jen při zjevném balastu**. Detekce HTML: **rozhodne agent**.
**Co fungovalo a proč:**
- `bookmark.py`: `deleted_at TEXT` + migrace (idempotentní ALTER, vzor jako `content`); `AND deleted_at IS NULL` do obou větví `_ordered_ids` + přímých SELECTů v `list`/`history` (žádný „subset" refaktor — unread/read je jen negace `read_at`, na připomínku uživatele); nový `cmd_delete` (soft, resolve z unread setu). Potvrzení je záměrně na úrovni agenta (SKILL.md), ne skriptu.
- `html_to_markdown.py`: nový PEP 723 helper (`dependencies=["trafilatura"]`, shebang `uv run --script`), stdin HTML → stdout markdown; prázdný vstup exit 1, nic neextrahováno exit 2. Agent ho **pipe-uje** do `bookmark.py add … --content-file -` → převedený markdown teče shellem, **neprochází kontextem modelu** (levné). Když trafilatura nic nevrátí → prázdný content → NULL → fallback na čištění modelem.
- `SKILL.md`: rozcestník HTML (pipe helper) vs plain text (uložit rovnou, čistit jen při balastu); delete + potvrzovací flow + display-ID caveat; při paywallu/nezdaru stažení agent **požádá uživatele o vložení článku** místo tichého fallbacku.
- Testy (10× zelené): soft delete (skryje, řádek zůstává), delete out-of-range, migrace `deleted_at`, `html_to_markdown` extrakce (boilerplate pryč, `#`/`##` nadpisy) + prázdný vstup. Lokálně ověřeno i na realistickém HTML (nav/reklama/patka odstraněny) a exact pipe konstrukt.
- Migrace `deleted_at` ověřena na kopii reálné serverové DB (16 řádků zachováno, NULL). Nasazeno `rsync -av --exclude=__pycache__`, owner `nanobot:nanobot`. E2E na serveru: HTML pipe → `content 1` vrátí čistý markdown; `delete 1` → soft (`deleted_at` set, řádek v DB). Testovací řádek pak hard-smazán přes `sqlite3` → zpět 16.
**Co zbývá:** Behaviorální ověření uživatelem v chatu (agent má u HTML pipe-ovat helper, u plain textu čistit jen při balastu, u paywallu požádat o vložení, před delete potvrdit). Známá omezení: `delete`/`content` jen z unread setu (`list`), ne z `history`; žádný restore (soft-smazané jen ručně přes DB); trafilatura potřebuje realistickou stránku — na drobném snippetu boilerplate nepozná (fallback na model to řeší).
**Jak vrátit zpět:** Repo `git revert` skill committu + `rsync` na server. Migrace (`deleted_at`) aditivní/nedestruktivní — sloupec může na serveru zůstat. Soft-smazané řádky mají `deleted_at` set; revert skriptu je zase zviditelní (data se neztratí).
## 2026-07-06 — remind: deterministická pojistka proti duplicitnímu textu + steering
**Cíl:** `/remind` občas místo jednoho reminderu s víc časy založil víc samostatných reminderů se stejným textem. Nanobot (Dream) na to reagoval přidáním jednoho příkladu do serverového `SKILL.md`. Uživatel se ptal, jestli to stačí — po rozboru padlo rozhodnutí přidat i tvrdou pojistku: jeden text nikdy nepotřebuje víc záznamů, takže `add`/`edit` mají duplicitu odmítat chybou.
**Důkaz příčiny (ne bug v kódu):** Serverový `log/reminder.log` — „UFO burger" `id=46` (18:00) + `id=47` (07:00) 7 s po sobě (split jednoho zadání); dřív totéž „note #work" `id=41/42/43`. CLI mechanismus byl přitom vždy správný: `--at`/`--cron` jsou `action="append"`, `insert_schedules` vloží všechny naráz → `add --at X --at Y` = jeden reminder, dva časy. Příčina = chování agenta, který zadání rozdělil do víc `add` volání.
**Co jsem udělal:**
- `store.py`: `find_active_by_exact_text(conn, text, exclude_id=None)` — porovnání Python-side `casefold()` na trimnutém textu (ne SQL `lower()`, to v SQLite nefolduje unicode diakritiku → „Čaj"/„čaj" by prošlo).
- `remind_cli.py`: helper `_reject_duplicate_text` (vrací `{"error":"duplicate text", display_id, hint}` na stderr, exit 1). Zapojen v `cmd_add` (před transakcí) a `cmd_edit` (při změně textu, `exclude_id=rid` → přejmenování na vlastní text projde). Hint navádí na recovery po splitu: `edit --id N --replace-schedules` se všemi časy.
- `SKILL.md`: do mapovací tabulky přidán cron multi-time řádek (`--cron … --cron …`), zachován serverový `--at`/`--at` příklad (dotažen do repa); do Behavioral contract explicitní pravidlo „jeden text = jeden reminder".
- Testy: 3 nové (`test_add_rejects_duplicate_text`, `test_add_duplicate_case_and_diacritics_insensitive`, `test_edit_text_collision_rejected`); 2 existující dup testy (`test_remove_by_id_disambiguates_duplicates`, `test_ambiguous_keyword_returns_display_ids`) staví duplicitu přes nový helper `_seed_duplicate` (přímý `store.insert_reminder`, obchází guard) — disambiguace pre-existujících dupů zůstává validní.
**Co fungovalo a proč:** 83 testů zelených. Lokální smoke (temp `REMIND_DB`): add#1 OK, add#2 stejný text → odmítnut (exit 1, display_id, hint), správná cesta `add --at … --at …` → jeden reminder se dvěma `at`. Před deployem ověřeno, že serverové skripty == git HEAD (žádná autonomní divergence Dreamem), SKILL.md se lišil jen o ten jeden `--at` řádek (dotažen). Nasazeno `rsync -av skills/remind/`, owner `nanobot:nanobot` ✓. Server == lokál u všech změněných souborů. Na serveru jsem mutace nespouštěl (`log_operation` ignoruje `REMIND_DB` → zapsalo by do reálného `reminder.log`).
**Co zbývá:** Zápis do `decisions.md` (návrh připraven, čeká na formulaci autora — ruší dosavadní rámování „duplicitní texty povoleny by design"). Behaviorální ověření dělá uživatel v chatu.
**Jak vrátit zpět:** Repo `git revert 61e7068` + `rsync` na server. Změna je čistě logika CLI (žádná migrace DB), revert je bezpečný.

View File

@@ -4,6 +4,12 @@ Ověřená fakta o vnitřním fungování nanobota. Stručně, s případným od
--- ---
## `uv` na serveru není v non-interaktivním SSH PATH
`ssh nanobot@nanobot.hell "uv run ..."` selže s `uv: command not found` — non-login shell nemá `~/.local/bin` v PATH. Plná cesta je `/home/nanobot/.local/bin/uv`. Cron skilly to obcházejí shebangem `#!/usr/bin/env -S uv run --script`. Zdroj: nasazení remind `upcoming` 2026-06-10 (history.md).
---
## Kdy je a není potřeba restart nanobot.service ## Kdy je a není potřeba restart nanobot.service
**Restart NENÍ potřeba:** **Restart NENÍ potřeba:**
@@ -49,6 +55,7 @@ Naplánované joby jsou v `/home/nanobot/.nanobot/workspace/cron/jobs.json`. Str
Změna se projeví **bez restartu** — cron service volá `_load_store()` při každém ticku (`nanobot/cron/service.py:394`), jobs.json se čte čerstvě. Hot reload tedy funguje out-of-box. Změna se projeví **bez restartu** — cron service volá `_load_store()` při každém ticku (`nanobot/cron/service.py:394`), jobs.json se čte čerstvě. Hot reload tedy funguje out-of-box.
**Editace:** Python in-place editor přes SSH, např.: **Editace:** Python in-place editor přes SSH, např.:
```bash ```bash
ssh root@nanobot.hell "python3 -c \" ssh root@nanobot.hell "python3 -c \"
import json; from pathlib import Path import json; from pathlib import Path
@@ -68,7 +75,7 @@ Nanobot cron job **vždy** běží přes agenta (`agent.process_direct`) — nea
1. **Prompt je obalený natvrdo v kódu.** `nanobot/cli/commands.py:on_cron_job` přilepí před `payload.message` fixní `"The scheduled time has arrived. Deliver this reminder to the user now…"`. Tvoje "exit silently" instrukce je s tím v konfliktu → agent improvizuje meta-odpověď ("Output was empty…"). 1. **Prompt je obalený natvrdo v kódu.** `nanobot/cli/commands.py:on_cron_job` přilepí před `payload.message` fixní `"The scheduled time has arrived. Deliver this reminder to the user now…"`. Tvoje "exit silently" instrukce je s tím v konfliktu → agent improvizuje meta-odpověď ("Output was empty…").
2. **`evaluate_response` je fail-open.** `nanobot/utils/evaluator.py` rozhoduje o doručení druhým LLM callem; při chybě / chybějícím tool-callu vrací `True` (doruč). Slabší modely často `"no tool call returned, defaulting to notify"` → meta-odpověď propadne na Telegram. Proto únik jen "sem tam" a pokaždé jinak formulovaný. 2. **`evaluate_response` je fail-open.** `nanobot/utils/evaluator.py` rozhoduje o doručení druhým LLM callem; při chybě / chybějícím tool-callu vrací `True` (doruč). Slabší modely často `"no tool call returned, defaulting to notify"` → meta-odpověď propadne na Telegram. Proto únik jen "sem tam" a pokaždé jinak formulovaný.
**Zamítnuto:** pouhá úprava promptu na "exit silently" (nestačí — viz body 1+2). **Zamítnuto:** pouhá úprava promptu na "exit silently" (nestačí — viz body 1+2).
**Fix:** doručování úplně mimo agenta — viz `/remind skill` níže (system crontab + přímé Bot API). **Fix:** doručování úplně mimo agenta — viz `/remind skill` níže (system crontab + přímé Bot API).
Plný rozbor: history 2026-05-27 "Spam Output was empty". Plný rozbor: history 2026-05-27 "Spam Output was empty".
@@ -174,23 +181,29 @@ Zdroj: `nanobot/channels/telegram.py:258-326` (BotCommand registrace, regex rout
**Telegram:** `/remind text` je bot command, nedojde k agentovi jako text. Psát přirozeně: `připomeň mi...`, `nastav připomínku...` **Telegram:** `/remind text` je bot command, nedojde k agentovi jako text. Psát přirozeně: `připomeň mi...`, `nastav připomínku...`
**Read-back doručených („co dnes přišlo?") = subcommand `delivered`**, ne čtení logu. `remind_edit.py delivered [--since YYYY-MM-DD]` dotáhne z `reminder_fires` jen doručené (`status='delivered'`), default dnes; `fire_time`/`delivered_at` se ukládají **Prague-naive**, takže žádná konverze. `TOOLS.md` na to směruje agenta. **Read-back doručených („co dnes přišlo?") = subcommand `delivered`**, ne čtení logu. `remind_cli.py delivered [--since YYYY-MM-DD]` dotáhne z `reminder_fires` jen doručené (`status='delivered'`), default dnes; `fire_time`/`delivered_at` se ukládají **Prague-naive**, takže žádná konverze. `TOOLS.md` na to směruje agenta.
**Soft delete:** `remove` nastaví `deleted_at` (záznam zůstane v DB, jen zmizí z `list` a odpalů); hard delete jen přímým DB zásahem. Cron tool se používá pouze pro background agent úlohy, nikdy pro osobní notifikace uživateli. **Soft delete:** `remove` nastaví `deleted_at` (záznam zůstane v DB, jen zmizí z `list` a odpalů); hard delete jen přímým DB zásahem. Cron tool se používá pouze pro background agent úlohy, nikdy pro osobní notifikace uživateli.
**Editace DB — vždy přes `remind_edit.py`** (deterministické CLI, SQLite transakce + validace cron/`at`/random), nikdy přímý DB nebo `edit_file`. Volat `uv run skills/remind/scripts/remind_edit.py <subcommand>` (workspace-relativní cesta, exec běží z workspace rootu). Subcommandy: `list`, `add --text … (--cron EXPR… | --at ISO… | --random-times-per-day N --random-window HH:MM-HH:MM [--random-days 1-5] [--random-from DATE] [--random-until DATE])`, `edit --keyword|--id [--text …] [--replace-schedules + nové schedule flagy]`, `remove`, `enable`, `disable`, `delivered`. Výběr záznamu přes `--keyword` (substring; ambiguous → vrátí ids) nebo `--id` (přesný). **Mutace vrací JSON** (`{"added": …}` ap.); **`list` vrací čitelný text** — řádek na reminder `#<id> text [enabled|disabled]` + odsazené schedule řádky (prefix `#<id>` je id pro `--id`), prázdný store → `(no active reminders)`. Chyby stderr + non-zero exit. Env `REMIND_DB` přepíše cestu k DB (testy). **Editace DB — vždy přes `remind_cli.py`** (deterministické CLI, SQLite transakce + validace cron/`at`/random), nikdy přímý DB nebo `edit_file`. Volat `uv run skills/remind/scripts/remind_cli.py <subcommand>` (workspace-relativní cesta, exec běží z workspace rootu). Subcommandy: `list`, `add --text … (--cron EXPR… | --at ISO… | --random-times-per-day N --random-window HH:MM-HH:MM [--random-days 1-5] [--random-from DATE] [--random-until DATE])`, `edit --keyword|--id [--text …] [--replace-schedules + nové schedule flagy]`, `remove`, `enable`, `disable`, `delivered`. Výběr záznamu přes `--keyword` (substring; ambiguous → vrátí ids) nebo `--id` (přesný). **Mutace vrací JSON** (`{"added": …}` ap.); **`list` vrací čitelný text** — řádek na reminder `#<id> text [enabled|disabled]` + odsazené schedule řádky (prefix `#<id>` je id pro `--id`), prázdný store → `(no active reminders)`. Chyby stderr + non-zero exit. Env `REMIND_DB` přepíše cestu k DB (testy). **Oprava/změna textu = `edit --id <n> --text "…"`** (id z `list`), NIKDY remove+add (ztratil by se delivery history a změnilo id) — SKILL.md k tomu agenta explicitně navádí. **Plný výčet flagů žije v argparse**, ne v SKILL.md (hybrid od 2026-06-10): `remind_cli.py <cmd> --help` (rename z `remind_edit.py`, history 2026-06-10).
**Gotcha — `log_operation` ignoruje `REMIND_DB`:** audit zápis do `workspace/log/reminder.log` jde přes `__file__`-relativní cestu, **ne** přes `DB_PATH`/`REMIND_DB`. Spuštění test suite **na serveru** proto zapíše fixture texty do reálného `reminder.log` (prod `reminder_fires` zůstává čistá — ta jede přes DB_PATH). **Testy spouštět jen lokálně** (`uv run --with pytest --with croniter pytest skills/remind/tests/`). **Gotcha — `log_operation` ignoruje `REMIND_DB`:** audit zápis do `workspace/log/reminder.log` jde přes `__file__`-relativní cestu, **ne** přes `DB_PATH`/`REMIND_DB`. Spuštění test suite **na serveru** proto zapíše fixture texty do reálného `reminder.log` (prod `reminder_fires` zůstává čistá — ta jede přes DB_PATH). **Testy spouštět jen lokálně** (`uv run --with pytest --with croniter pytest skills/remind/tests/`).
**Náhodný (deterministický) čas (`random` blok):** N× denně v náhodný čas uvnitř okna, ale deterministicky — sdílený modul `scripts/random_times.py` počítá časy ze seedu `f"{datum}|{text}"`, takže sender zůstává bezstavový (počítá se každou minutu znovu). Min. rozestup mezi časy = konstanta `MIN_GAP_MIN` (default 15) v tomtéž modulu. Validace v `remind_edit.py` jde přes stejný `compute_fire_times`. Testy: `uv run --with pytest pytest skills/remind/tests/`. Návrh: [plans/remind-random-time.md](plans/remind-random-time.md). **Náhodný (deterministický) čas (`random` blok):** N× denně v náhodný čas uvnitř okna, ale deterministicky — sdílený modul `scripts/random_times.py` počítá časy ze seedu `f"{datum}|{text}"`, takže sender zůstává bezstavový (počítá se každou minutu znovu). Min. rozestup mezi časy = konstanta `MIN_GAP_MIN` (default 15) v tomtéž modulu. Validace v `remind_cli.py` jde přes stejný `compute_fire_times`. (`random_times.py` taky drží sdílené `minutes_to_hhmm` — dřív duplikované v cli i send.) Testy: `uv run --with pytest pytest skills/remind/tests/`. Návrh: [plans/remind-random-time.md](plans/remind-random-time.md).
**Týdenní náhodný režim (`period='week'`, od 2026-06-15):** `--random-times-per-week N` (mutually exclusive s `--random-times-per-day`) rozprostře N odpálení přes týden PoNe na N **různých** náhodných dnů, 1 čas na den uvnitř okna. Engine zůstává **per-day volaný**`_weekly_fire_times` spočítá celý týdenní plán deterministicky ze seedu `f"{pondělí}|{text}|week"` (ne ze dne) a vrátí jen fires daného dne, takže daemon i forecast nemění iterační logiku. Sloupec `schedule_random.period` ('day'/'week', default 'day') + idempotentní `_migrate(conn)` v `db.get_db` (PRAGMA-guarded `ALTER TABLE ADD COLUMN`, protože `init_db` na živou DB nesahá). `count > dostupné dny` se clampuje (hraniční týden u from/until), `count > kapacita filtru` raisuje. Detaily: history 2026-06-15.
**Display IDs (autonomní serverová feature, do repa dotaženo 2026-06-15):** `list`/`upcoming` ukazují **1-based pozici** mezi aktivními remindery (`#1`, `#2`…, počítáno on-the-fly v `_active_display_order`), **ne** interní DB id. `--id` i chybové hlášky (`{"error":"no match","display_id":n}`, `ambiguous` s `display_id`) pracují s display ID. Přečíslovává se po každém `remove` → při nejistotě vždy nejdřív `list`. Interní id se uživateli nikdy neukazuje. (Objeveno při syncu před weekly deployem — feature byla na serveru, ne v repu; pravděpodobně Dream procesor. history 2026-06-15.)
**uv path na serveru:** `/home/nanobot/.local/bin/uv` — není v PATH pro root. Spouštět jako `/home/nanobot/.local/bin/uv run script.py`. **uv path na serveru:** `/home/nanobot/.local/bin/uv` — není v PATH pro root. Spouštět jako `/home/nanobot/.local/bin/uv run script.py`.
**DB_PATH path (gotcha):** V `remind_send.py`/`remind_edit.py` je `Path(__file__).resolve().parent.parent.parent.parent`**4 levely** nahoru z `.../workspace/skills/remind/scripts/` na workspace root, pak `/db/reminders.sqlite`. Se 3 levely míří cesta mimo (`.../workspace/skills/`) a DB se vytvoří/hledá na špatném místě. Override přes env `REMIND_DB`. **DB_PATH path (gotcha):** V `remind_send.py`/`remind_cli.py` je `Path(__file__).resolve().parent.parent.parent.parent`**4 levely** nahoru z `.../workspace/skills/remind/scripts/` na workspace root, pak `/db/reminders.sqlite`. Se 3 levely míří cesta mimo (`.../workspace/skills/`) a DB se vytvoří/hledá na špatném místě. Override přes env `REMIND_DB`.
**jobs.json se nepersistuje přes restart agenta:** Změny v `cron/jobs.json` provedené agentem přes `edit_file` tool se mohou ztratit po restartu service (nanobot drží jobs v paměti a přepisuje soubor). Bezpečnější: editovat Python in-place přes SSH + ihned restartovat service. **jobs.json se nepersistuje přes restart agenta:** Změny v `cron/jobs.json` provedené agentem přes `edit_file` tool se mohou ztratit po restartu service (nanobot drží jobs v paměti a přepisuje soubor). Bezpečnější: editovat Python in-place přes SSH + ihned restartovat service.
**Identita reminderu = `id` (SQLite autoincrement).** `text` slouží jako lidský klíč pro `--keyword` (substring match), ale nejednoznačné/duplicitní texty se řeší `--id` (z `list` nebo z `ambiguous` chyby, která ids vypíše). Dedup je per `reminder_id`+`schedule_id`, takže i stejné texty se odpalují nezávisle. (Historie: per-entry ID bylo nad YAML zvažováno a zavrženo — history 2026-06-02; migrace na SQLite ho zavedla nativně.) **Identita reminderu = `id` (SQLite autoincrement).** `text` slouží jako lidský klíč pro `--keyword` (substring match); jednoznačný výběr přes `--id`. Dedup **odpalů** je per `reminder_id`+`schedule_id`. (Historie: per-entry ID bylo nad YAML zvažováno a zavrženo — history 2026-06-02; migrace na SQLite ho zavedla nativně.)
**Aktivní texty jsou unikátní (guard od 2026-07-06).** `add` i `edit --text` **odmítnou** duplicitní text aktivního reminderu (`find_active_by_exact_text`, casefold vč. diakritiky) chybou `{"error":"duplicate text", display_id, hint}` (exit 1). Důvod: agent občas rozdělil „jeden text, víc časů" do víc `add` volání místo jednoho `add` s opakovanými `--at`/`--cron` (doloženo: UFO burger `id=46/47`). Jeden text = jeden reminder s víc schedule. `--id`/`ambiguous` disambiguace zůstává — substring keyword může být nejednoznačný napříč **různými** texty a historické dupy (před guardem) v DB pořád jsou. Plný záznam: history 2026-07-06.
### Vyřešené chyby ### Vyřešené chyby
@@ -206,6 +219,7 @@ Modely se přidávají jako položky do `modelPresets` v `~/.nanobot/config.json
1. **Ověř dostupnost u providera.** Pro Ollama: `curl http://nvidia.hell:11434/api/tags` a zkontroluj, že název modelu (přesně, včetně `:cloud` suffixu) je v seznamu. Pro OpenRouter: `curl https://openrouter.ai/api/v1/models`. 1. **Ověř dostupnost u providera.** Pro Ollama: `curl http://nvidia.hell:11434/api/tags` a zkontroluj, že název modelu (přesně, včetně `:cloud` suffixu) je v seznamu. Pro OpenRouter: `curl https://openrouter.ai/api/v1/models`.
2. **Edituj config in-place** přes Python (zachová ostatní klíče včetně secrets): 2. **Edituj config in-place** přes Python (zachová ostatní klíče včetně secrets):
```bash ```bash
ssh nanobot@nanobot.hell 'python3 -c " ssh nanobot@nanobot.hell 'python3 -c "
import json, pathlib import json, pathlib
@@ -215,10 +229,13 @@ Modely se přidávají jako položky do `modelPresets` v `~/.nanobot/config.json
p.write_text(json.dumps(c, indent=2)) p.write_text(json.dumps(c, indent=2))
"' "'
``` ```
3. **Restartuj službu**, aby gateway preset načetla: 3. **Restartuj službu**, aby gateway preset načetla:
```bash ```bash
ssh nanobot@nanobot.hell 'XDG_RUNTIME_DIR=/run/user/1000 systemctl --user restart nanobot.service' ssh nanobot@nanobot.hell 'XDG_RUNTIME_DIR=/run/user/1000 systemctl --user restart nanobot.service'
``` ```
4. **V chatu** (Telegram/WebUI) přepneš příkazem `/model <preset-name>`. 4. **V chatu** (Telegram/WebUI) přepneš příkazem `/model <preset-name>`.
**Konvence pojmenování presetů:** `<model-zkratka>-<provider>` (např. `kimi-k2.6-openrouter`, `glm-5.1-ollama`). Suffix providera je důležitý — uživatel chce v názvu vidět, odkud model jede. **Konvence pojmenování presetů:** `<model-zkratka>-<provider>` (např. `kimi-k2.6-openrouter`, `glm-5.1-ollama`). Suffix providera je důležitý — uživatel chce v názvu vidět, odkud model jede.
@@ -279,7 +296,7 @@ Zdroj: `nanobot/agent/skills/`, `ContextBuilder._load_bootstrap_files()`
## Skill `description` — k čemu reálně slouží (progressive loading) ## Skill `description` — k čemu reálně slouží (progressive loading)
Pole `description` ve frontmatteru non-always skillu je **routing signál**, ne kontext „jak skill funguje". Při sestavování system promptu se každý non-always skill vykreslí jako **jeden řádek** v seznamu: `- **<name>** — <description> \`cesta/k/SKILL.md\``. Tělo SKILL.md se načte **až on-demand**, když si agent skill sám přečte přes `read_file`. Důsledky: Pole `description` ve frontmatteru non-always skillu je **routing signál**, ne kontext „jak skill funguje". Při sestavování system promptu se každý non-always skill vykreslí jako **jeden řádek** v seznamu: `- **<name>** — <description> \`cesta/k/SKILL.md\``. Tělo SKILL.md se načte **až on-demand**, když si agent skill sám přečte přes`read_file`. Důsledky:
- `description` je jediná info o skillu v promptu, dokud agent nečte tělo → patří tam jen *kdy/proč* skill spustit (trigger fráze, odlišení od příbuzných skillů), **ne** *jak* funguje. - `description` je jediná info o skillu v promptu, dokud agent nečte tělo → patří tam jen *kdy/proč* skill spustit (trigger fráze, odlišení od příbuzných skillů), **ne** *jak* funguje.
- `description` se **nezkracuje** (`_get_skill_description` vrací text doslova) a je v promptu **každý tah** u všech skillů → trvalý token cost. Drž stručně, routing-orientovaně. Detailní postup patří do těla. - `description` se **nezkracuje** (`_get_skill_description` vrací text doslova) a je v promptu **každý tah** u všech skillů → trvalý token cost. Drž stručně, routing-orientovaně. Detailní postup patří do těla.
@@ -379,6 +396,7 @@ už používá `/remind` (viz "Cron job s LLM agentem je nespolehlivý…" výš
Architektura podobná `/remind` — orchestrace mimo agent loop, žádný cron preamble. Architektura podobná `/remind` — orchestrace mimo agent loop, žádný cron preamble.
**Tok:** **Tok:**
1. Skill `detach` v chatu → `exec skills/detach/scripts/create-task.py --goal … --slug … --channel … --chat-id …`. Skript vygeneruje timestamp + frontmatter, zajistí fronty (`mkdir -p`), atomicky zapíše do `tasks/tmp/` a přesune do `tasks/inbox/` (atomický rename, partial-write race neexistuje). Agent dělá jen LLM části (přeformulovat goal, vybrat slug, přečíst channel/chat_id z runtime contextu) — žádný ruční `write_file`/`mv`/`date`. 1. Skill `detach` v chatu → `exec skills/detach/scripts/create-task.py --goal … --slug … --channel … --chat-id …`. Skript vygeneruje timestamp + frontmatter, zajistí fronty (`mkdir -p`), atomicky zapíše do `tasks/tmp/` a přesune do `tasks/inbox/` (atomický rename, partial-write race neexistuje). Agent dělá jen LLM části (přeformulovat goal, vybrat slug, přečíst channel/chat_id z runtime contextu) — žádný ruční `write_file`/`mv`/`date`.
2. Systemd user unit `tasks-daemon.path` (`DirectoryNotEmpty=…/tasks/inbox`) přes inotify spustí `tasks-daemon.service` (`Type=oneshot`). 2. Systemd user unit `tasks-daemon.path` (`DirectoryNotEmpty=…/tasks/inbox`) přes inotify spustí `tasks-daemon.service` (`Type=oneshot`).
3. `tasks-daemon.py` (Python, shebang na uv venv interpreter) projede inbox: `mv → running/`, parse frontmatter (`chat_id` povinný), zavolá `Nanobot.from_config().run(goal, session_key=f"detach:<stem>")` s 45-min timeoutem, appendne `## Result` sekci, `mv → done/` nebo `failed/`, pošle Telegram zprávu přes Bot API (urllib + token z `~/.nanobot/config.json["channels"]["telegram"]["token"]`). 3. `tasks-daemon.py` (Python, shebang na uv venv interpreter) projede inbox: `mv → running/`, parse frontmatter (`chat_id` povinný), zavolá `Nanobot.from_config().run(goal, session_key=f"detach:<stem>")` s 45-min timeoutem, appendne `## Result` sekci, `mv → done/` nebo `failed/`, pošle Telegram zprávu přes Bot API (urllib + token z `~/.nanobot/config.json["channels"]["telegram"]["token"]`).
@@ -386,6 +404,7 @@ Architektura podobná `/remind` — orchestrace mimo agent loop, žádný cron p
**Volba modelu pro task (od 2026-06-07):** Detach umí task spustit na explicitně zvoleném presetu (background = latence nebolí, vyplatí se silnější model). Uživatel model jen zmíní ve větě („na kimi") → agent předá token jako `create-task.py --model "<token>"` → skript ho **při captue** fuzzy-resolvne proti `config.json` (`resolve_preset`: exact case-insensitive → unikátní substring; jinak `KeyError` se seznamem, exit 1, fail-fast v chatu) a uloží přesný preset do frontmatteru `model:`. Bez `--model` jede default (`agents.defaults.modelPreset`). Daemon přečte `fm["model"]` a před `run()` přepne `bot._loop.set_model_preset(preset)` — stejný switch jako `/model` v chatu (ověřeno e2e s nainstalovaným balíčkem, history 2026-06-07). **Gotcha:** klíč presetů je v serverovém `config.json` na disku **snake_case `model_presets`** (ne camelCase `modelPresets`), zatímco `agents.defaults.modelPreset` je camelCase — `load_preset_names()` proto čte oba tvary. **Volba modelu pro task (od 2026-06-07):** Detach umí task spustit na explicitně zvoleném presetu (background = latence nebolí, vyplatí se silnější model). Uživatel model jen zmíní ve větě („na kimi") → agent předá token jako `create-task.py --model "<token>"` → skript ho **při captue** fuzzy-resolvne proti `config.json` (`resolve_preset`: exact case-insensitive → unikátní substring; jinak `KeyError` se seznamem, exit 1, fail-fast v chatu) a uloží přesný preset do frontmatteru `model:`. Bez `--model` jede default (`agents.defaults.modelPreset`). Daemon přečte `fm["model"]` a před `run()` přepne `bot._loop.set_model_preset(preset)` — stejný switch jako `/model` v chatu (ověřeno e2e s nainstalovaným balíčkem, history 2026-06-07). **Gotcha:** klíč presetů je v serverovém `config.json` na disku **snake_case `model_presets`** (ne camelCase `modelPresets`), zatímco `agents.defaults.modelPreset` je camelCase — `load_preset_names()` proto čte oba tvary.
**Soubory:** **Soubory:**
- `~/.nanobot/workspace/skills/detach/SKILL.md` — definice + triggery (EN-only) - `~/.nanobot/workspace/skills/detach/SKILL.md` — definice + triggery (EN-only)
- `~/.nanobot/workspace/skills/detach/scripts/tasks_common.py` — sdílené čisté helpery (TASKS, FILENAME_RE, parse_frontmatter, parse_kv, format_*, build_task_*), importují ho ostatní skripty - `~/.nanobot/workspace/skills/detach/scripts/tasks_common.py` — sdílené čisté helpery (TASKS, FILENAME_RE, parse_frontmatter, parse_kv, format_*, build_task_*), importují ho ostatní skripty
- `~/.nanobot/workspace/skills/detach/scripts/create-task.py` — capture skript (frontmatter + atomický tmp→inbox) - `~/.nanobot/workspace/skills/detach/scripts/create-task.py` — capture skript (frontmatter + atomický tmp→inbox)
@@ -433,7 +452,7 @@ Daemon notifikuje **jen Telegram** (přes Bot API, deterministicky). Když task
Když skill volá `exec` bez explicitního `working_dir`, příkaz běží s **CWD = workspace root** (`~/.nanobot/workspace`), **ne** v adresáři skillu. Cesty na skripty skillu proto musí být buď workspace-relativní (`skills/<name>/scripts/x.py`) nebo absolutní — **skill-dir-relativní `scripts/x.py` se rozbije** (resolvuje na `workspace/scripts/x.py`). Když skill volá `exec` bez explicitního `working_dir`, příkaz běží s **CWD = workspace root** (`~/.nanobot/workspace`), **ne** v adresáři skillu. Cesty na skripty skillu proto musí být buď workspace-relativní (`skills/<name>/scripts/x.py`) nebo absolutní — **skill-dir-relativní `scripts/x.py` se rozbije** (resolvuje na `workspace/scripts/x.py`).
Zdroj: upstream `nanobot/agent/tools/shell.py:148` (`working_dir=ctx.workspace`) + `:370` (`cwd = working_dir or workspace_root`). Pozn.: remind SKILL.md používá `scripts/remind_edit.py` — v tomto ohledu zavádějící; detach používá korektní `skills/detach/scripts/…`. Zdroj: upstream `nanobot/agent/tools/shell.py:148` (`working_dir=ctx.workspace`) + `:370` (`cwd = working_dir or workspace_root`). Pozn.: remind SKILL.md používá workspace-relativní `skills/remind/scripts/remind_cli.py` (korektní); detach analogicky `skills/detach/scripts/…`.
--- ---
@@ -493,6 +512,7 @@ Install: `systemctl --user enable --now <unit>.path`. Lingering musí být zapnu
**Gotcha — level-triggered `.path` + startup crash = permanentní latch:** Když oneshot daemon spadne **ve startup fázi** (před vyprázdněním inboxu), inbox zůstane neprázdný → `.path` ho hned znovu spustí → další pád → … Na manager defaultu (`StartLimitIntervalSec=10s`, `Burst=5`) to za <2 s narazí na rate-limit a systemd zalatchuje **`.service` i `.path`** do `failed (unit-start-limit-hit)`. Z toho se **sám nezotaví** — nutný `systemctl --user reset-failed <unit>.service <unit>.path` + `restart <unit>.path`. (Stalo se 7.6., když daemon padal na `NameError`.) **Gotcha — level-triggered `.path` + startup crash = permanentní latch:** Když oneshot daemon spadne **ve startup fázi** (před vyprázdněním inboxu), inbox zůstane neprázdný → `.path` ho hned znovu spustí → další pád → … Na manager defaultu (`StartLimitIntervalSec=10s`, `Burst=5`) to za <2 s narazí na rate-limit a systemd zalatchuje **`.service` i `.path`** do `failed (unit-start-limit-hit)`. Z toho se **sám nezotaví** — nutný `systemctl --user reset-failed <unit>.service <unit>.path` + `restart <unit>.path`. (Stalo se 7.6., když daemon padal na `NameError`.)
**Hardening (ověřeno, nasazeno na tasks-daemon):** v `.service` přidat **Hardening (ověřeno, nasazeno na tasks-daemon):** v `.service` přidat
```ini ```ini
[Unit] [Unit]
StartLimitIntervalSec=1800 StartLimitIntervalSec=1800
@@ -501,6 +521,7 @@ StartLimitBurst=20
Restart=on-failure Restart=on-failure
RestartSec=60 RestartSec=60
``` ```
`Restart=on-failure` + `RestartSec` dá **delay mezi pokusy** (nezávisle na `.path` retriggeru); čistý `exit 0` (inbox vyprázdněn) ani SIGTERM od systemd nerestartují. Širší okno (`30min`/`20`) zajistí, že se latch po posunu okna sám pustí dál. **`man systemd.service`: pro `Type=oneshot` jsou zakázané jen `Restart=always`/`on-success`, `on-failure` je povolený.** `Restart=on-failure` + `RestartSec` dá **delay mezi pokusy** (nezávisle na `.path` retriggeru); čistý `exit 0` (inbox vyprázdněn) ani SIGTERM od systemd nerestartují. Širší okno (`30min`/`20`) zajistí, že se latch po posunu okna sám pustí dál. **`man systemd.service`: pro `Type=oneshot` jsou zakázané jen `Restart=always`/`on-success`, `on-failure` je povolený.**
Zdroj: `man systemd.path` + `man systemd.service`, ověřeno smoke testem před deployem detach skillu; latch+hardening history 2026-06-07 19:33. Zdroj: `man systemd.path` + `man systemd.service`, ověřeno smoke testem před deployem detach skillu; latch+hardening history 2026-06-07 19:33.
@@ -536,13 +557,19 @@ On-demand skill pro okamžitou explicitní paměť. Uživatel řekne „keep X"
--- ---
## Skill `/note` — explicit notes (no auto-load) ## Skill `/note` — osobní znalostní báze (capture → compile, od 2026-07-01)
On-demand skill pro ukládání poznámek do `workspace/notes.md`. Uživatel řekne „note X" → agent reformuluje na terse fact → zapíše jako bullet. Bez dedup, bez kompakce, bez dat. Přepsáno z SQLite row-store na **capture → compile pipeline** (vzor llm-wiki/detach, ale lehčí: jeden dokument, žádný index/graph/lint). Plná historie: history 2026-07-01. Plán: [plans/note-prepis.md](plans/note-prepis.md).
**Klíčový rozdíl od `/keep`:** `notes.md` nemá referenci v `USER.md` ani jiném bootstrap souboru — nikdy nevstupuje do context window automaticky. Maže se výhradně přes `/note delete <pattern>` (by index nebo substring). - **Úložiště (bez DB):** `notes/notes.md` = JEDEN strukturovaný dokument s tematickými sekcemi (`##`), které řídí LLM. Fronta zachytů = soubory v `notes/inbox/`, po zpracování → `notes/done/`, zadržené (paywall/nečitelné) → `notes/hard/`. Audit každého zachytu v `log/note.log`.
- **Dual-mode:** `/note <text>` = **okamžitě** (capture + inline compile v témže tahu, default); `/note cron <text>` = **odloženě** (jen capture, zpracuje cron). Oba volají `note_capture.py` (dumb atomic zápis) + sdílený *compile workflow* v SKILL.md + sdílený `notes/.compile.lock`.
- **Compile:** `note_compile.py` (systémový crontab uživatele `nanobot`, každou minutu) — levný fs pre-check bez importu nanobota, lockfile, `Nanobot.from_config().run(DRAIN_GOAL)` (process_direct bez cron preamble). Reformuluje na terse fakta, URL stáhne přes `web` tool + detekce paywallu, zařadí do sekce, přesune zdroj do `done/`/`hard/`.
- **Search:** `/note search|find <dotaz>` → načti celý `notes.md`, odpověz (read-only). Mazání = editace dokumentu (žádné display ID, žádné tagy).
- **Odděleno** od `/keep`, `MEMORY.md`, Dream, llm-wiki (`cml/`). **Kolize triggerů s `keep`:** NL „poznamenej si/ulož si …" může spadnout do `keep` (obě to claimují) — spolehlivý je explicitní `/note` prefix; description note skillu odlišen („NOT durable facts → keep"). Starý `note.py`/`note.sqlite` vyřazeny (`note.sqlite` v `backup/note-retired/`).
**Kde žije:** `workspace/notes.md`. Edituje ho výhradně `/note` skill. Odděleno od `/keep`, Dream, MEMORY.md — žádný cross-read ani cross-write. ## Nanobot MÁ `web` tool (search + Jina Reader fetch) — `TOOLS.md` ho nezmiňuje
Config `tools.web` (server `config.json`) má `enable: true` s **search** (`provider: duckduckgo`, maxResults 5) a **fetch** (`useJinaReader: true`). Agent tedy umí stáhnout URL přes `web` tool — Jina Reader vrací čistý markdown a zvládne JS i měkké paywally. `workspace/TOOLS.md` (dokumentace toolů) `web` **neuvádí** (jen `exec`/`grep`/`cron`) — je neúplný; zdroj pravdy je `config.json`. Pro fetch URL ve skillu preferuj `web` tool; `exec`+curl (`-o file` kvůli 10k/60s limitu `exec`) je fallback. Ověřeno při přepisu `/note` (DT Glass URL reálně stažena, history 2026-07-01).
--- ---
@@ -578,6 +605,7 @@ Nanobot má **hardcoded default `context_window_tokens = 65_536`** pro `ModelPre
Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5 / nemotron-3-super 262144, minimax-m2.7 204800, glm-5.1 196608, deepseek-v4-flash 1048576) + `maxTokens` 16384. **Bez restartu** — `modelPresets` se hot-reloadují (viz sekce „Kdy je a není potřeba restart"). U `:cloud` modelů hostí kontext Ollama cloud, takže `contextWindowTokens` reálně rozšíří budget — není to lokální `num_ctx` žeroucí RAM. Plný záznam: history 2026-06-02. Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5 / nemotron-3-super 262144, minimax-m2.7 204800, glm-5.1 196608, deepseek-v4-flash 1048576) + `maxTokens` 16384. **Bez restartu** — `modelPresets` se hot-reloadují (viz sekce „Kdy je a není potřeba restart"). U `:cloud` modelů hostí kontext Ollama cloud, takže `contextWindowTokens` reálně rozšíří budget — není to lokální `num_ctx` žeroucí RAM. Plný záznam: history 2026-06-02.
**Důsledky (trade-off, ne čistá výhra):** **Důsledky (trade-off, ne čistá výhra):**
- **+** Méně ořezávání/komprese historie → lepší návaznost v dlouhých sezeních. Delší souvislé odpovědi (16k vs 8k output). - **+** Méně ořezávání/komprese historie → lepší návaznost v dlouhých sezeních. Delší souvislé odpovědi (16k vs 8k output).
- **** „Lost in the middle": LLM neudrží kvalitu rovnoměrně přes celý kontext; info zahrabané uprostřed ~200k se vybavuje hůř. Propad je výraznější u slabších MoE modelů (glm/qwen/nemotron) než u špičkových (Kimi K2.6). Roste latence i protečené tokeny úměrně naplnění. - **** „Lost in the middle": LLM neudrží kvalitu rovnoměrně přes celý kontext; info zahrabané uprostřed ~200k se vybavuje hůř. Propad je výraznější u slabších MoE modelů (glm/qwen/nemotron) než u špičkových (Kimi K2.6). Roste latence i protečené tokeny úměrně naplnění.
- Při běžném (nízkém) naplnění se kvalita **nemění** — efekt nastává až když sezení přeroste 65k. - Při běžném (nízkém) naplnění se kvalita **nemění** — efekt nastává až když sezení přeroste 65k.
@@ -597,6 +625,7 @@ Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5
**Zapisovatelné lokace uvnitř sandboxu:** pouze workspace (persistentní) a `/tmp` (smaže se po příkazu). Cachové a datové adresáře `uv` (`~/.cache/uv`, `~/.local/share/uv`) jsou nedostupné — i kdyby byl `uv` system-wide, stahované balíčky by padaly nebo šly do `/tmp` a mizely. **Zapisovatelné lokace uvnitř sandboxu:** pouze workspace (persistentní) a `/tmp` (smaže se po příkazu). Cachové a datové adresáře `uv` (`~/.cache/uv`, `~/.local/share/uv`) jsou nedostupné — i kdyby byl `uv` system-wide, stahované balíčky by padaly nebo šly do `/tmp` a mizely.
**Možná řešení (bez modifikace zdrojáků nanobotu):** **Možná řešení (bez modifikace zdrojáků nanobotu):**
- Symlinky / kopie binárky do `/usr/local/bin/` (ekvivalent Docker image) - Symlinky / kopie binárky do `/usr/local/bin/` (ekvivalent Docker image)
- bwrap wrapper skript (nahradí `/usr/bin/bwrap` shellem, který přidá extra `--ro-bind-try` argumenty před předáním volání dál) — funkční, ale ovlivní všechna `bwrap` volání na systému - bwrap wrapper skript (nahradí `/usr/bin/bwrap` shellem, který přidá extra `--ro-bind-try` argumenty před předáním volání dál) — funkční, ale ovlivní všechna `bwrap` volání na systému
@@ -606,12 +635,28 @@ Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5
Zdroj: `nanobot/agent/tools/sandbox.py`, `Dockerfile` (ověřeno 2026-06-05). Zdroj: `nanobot/agent/tools/sandbox.py`, `Dockerfile` (ověřeno 2026-06-05).
**Korekce (2026-07-02): na produkčním `nanobot.hell` je sandbox VYPNUTÝ, takže pip závislosti přes `uv run` fungují.** Server má v `config.json` `exec` blok `"sandbox": ""` + `pathAppend` na `~/.local/bin/uv`, takže `exec` běží bez bwrap — stejné prostředí jako plain SSH. Skilly reálně jedou se závislostmi (`remind`→croniter, `llm-wiki`→pyyaml) a `bookmark`/`html_to_markdown.py` používá `trafilatura` (v2.1.0 ověřeno naživo: `uv run --with trafilatura` stáhne a naimportuje). Limity výše platí jen pro nasazení se *zapnutým* bwrap (`"sandbox": "bwrap"`), typicky Docker. Zdroj: live test 2026-07-02, viz `history.md` téhož dne.
--- ---
## Rozpad input contextu (co zabírá tokeny každý tah) ## Rozpad input contextu (co zabírá tokeny každý tah)
Detailní naměřený rozpad ~15k baseline input contextu (system prompt po částech + tool definitions 18 toolů) je v samostatném souboru [`tokens-explain.md`](tokens-explain.md) — k 0.2.1, preset glm-5.1. Stručně: ~8,7k system prompt (největší `MEMORY.md`, `skills_section`, `SOUL.md`), ~5,2k tool defs, zbytek session zprávy. Detailní naměřený rozpad ~15k baseline input contextu (system prompt po částech + tool definitions 18 toolů) je v samostatném souboru [`tokens-explain.md`](tokens-explain.md) — k 0.2.1, preset glm-5.1. Stručně: ~8,7k system prompt (největší `MEMORY.md`, `skills_section`, `SOUL.md`), ~5,2k tool defs, zbytek session zprávy.
## llm-wiki: zaseknutí způsobuje LLM v mechanické smyčce, ne pomalost
Při wiki lint/fix operacích se agent zasekl proto, že dělal **mechanickou práci v LLM smyčce** (čte stránku → hledá problém → edituje → další stránka). Správné řešení přesouvá tuto práci do deterministického skriptu — LLM rozhoduje jen tam, kde je skutečně potřeba úsudek.
**Praktické důsledky pro llm-wiki:**
- Lint musí vracet `(soubor, řádek, návrh opravy)` — ne jen "9 broken refs" → agent nemusí číst žádnou stránku
- `--fix` mód má smysl jen pro mechanické triviality (chybějící frontmatter pole ze šablony) kde není úsudek
- Slug collision a frontmatter šablona patří do compile pipeline, ne do lintu ex post
**Zamítnuté návrhy ze stejného důvodu:** timeout na každý krok, "aspoň jeden edit po každém čtení" (tlačí na editaci), agentní wiki fix subcommand (opět smyčka). Viz todo.md sekce "llm-wiki skill — další zlepšení".
---
## Optimalizovat skilly kvůli tokenům se nevyplatí ## Optimalizovat skilly kvůli tokenům se nevyplatí
Celý blok skillů (~2,5k: `skills_section` 1,56k + always-skilly 0,95k) je při okně 196k jen **~1,3 % okna**. Smazat on-demand skill ušetří jen popis + framing (~4075 tok/kus) → fakticky neměřitelné. **Description neškrtat** — je to trigger pro progressive loading (model podle něj pozná, kdy skill načíst); bez něj skill přestane fungovat, ušetříš desítky tokenů a přijdeš o funkčnost. Jediná páka jsou `always: true` (jdou celým tělem), ale `my`+`memory` mají být always. Větší blok jsou tool defs (5,2k, jen vypnutím toolů v configu). **Závěr:** skilly maž podle užitečnosti, ne kvůli tokenům; reálný strop je `contextWindowTokens`, ne baseline. Začalo by to dávat smysl až u desítekstovek skillů nebo velkého těla jako `always`. Plný rozbor: [`tokens-explain.md`](tokens-explain.md). Celý blok skillů (~2,5k: `skills_section` 1,56k + always-skilly 0,95k) je při okně 196k jen **~1,3 % okna**. Smazat on-demand skill ušetří jen popis + framing (~4075 tok/kus) → fakticky neměřitelné. **Description neškrtat** — je to trigger pro progressive loading (model podle něj pozná, kdy skill načíst); bez něj skill přestane fungovat, ušetříš desítky tokenů a přijdeš o funkčnost. Jediná páka jsou `always: true` (jdou celým tělem), ale `my`+`memory` mají být always. Větší blok jsou tool defs (5,2k, jen vypnutím toolů v configu). **Závěr:** skilly maž podle užitečnosti, ne kvůli tokenům; reálný strop je `contextWindowTokens`, ne baseline. Začalo by to dávat smysl až u desítekstovek skillů nebo velkého těla jako `always`. Plný rozbor: [`tokens-explain.md`](tokens-explain.md).
@@ -623,6 +668,7 @@ Celý blok skillů (~2,5k: `skills_section` 1,56k + always-skilly 0,95k) je při
**`maxTokens`** jde jednak přímo do API jako strop výstupu (`runner.py:621`), jednak se **odečítá z input budgetu** jako rezerva na výstup — u snipu historie i u konsolidace: `budget = contextWindowTokens maxTokens 1024` (`runner.py:1262`, `memory.py:619`). Vyšší `maxTokens` tedy zmenšuje prostor pro kontext a uspíší konsolidaci → držet skromně (16k OK), u reasoning modelů víc (reasoning tokeny se počítají taky). **`maxTokens`** jde jednak přímo do API jako strop výstupu (`runner.py:621`), jednak se **odečítá z input budgetu** jako rezerva na výstup — u snipu historie i u konsolidace: `budget = contextWindowTokens maxTokens 1024` (`runner.py:1262`, `memory.py:619`). Vyšší `maxTokens` tedy zmenšuje prostor pro kontext a uspíší konsolidaci → držet skromně (16k OK), u reasoning modelů víc (reasoning tokeny se počítají taky).
**Prompt caching** nanobot zapíná jen pro providery s `supports_prompt_caching=True` = **openrouter, anthropic, bedrock** (`registry.py:149,278`); `ollama` a `gemini` ne → aktivní `glm-5.1` přes ollama od nanobota **žádné cache breakpointy nedostává**. Háčky tam, kde caching jede: **Prompt caching** nanobot zapíná jen pro providery s `supports_prompt_caching=True` = **openrouter, anthropic, bedrock** (`registry.py:149,278`); `ollama` a `gemini` ne → aktivní `glm-5.1` přes ollama od nanobota **žádné cache breakpointy nedostává**. Háčky tam, kde caching jede:
1. **TTL 5 min** — holé `{"type": "ephemeral"}` (`anthropic_provider.py:400`). U sporadického chatu cache mezi tahy obvykle vyprší → platí se plný vstup; write navíc 1,25× base (read 0,1×). 1. **TTL 5 min** — holé `{"type": "ephemeral"}` (`anthropic_provider.py:400`). U sporadického chatu cache mezi tahy obvykle vyprší → platí se plný vstup; write navíc 1,25× base (read 0,1×).
2. **Konsolidace/snip rozbíjí prefix** — breakpoint sedí na system + `messages[-2]` + tools (`openai_compat:453`); jakmile Dream/`_snip_history` změní začátek pole, prefix se invaliduje. 2. **Konsolidace/snip rozbíjí prefix** — breakpoint sedí na system + `messages[-2]` + tools (`openai_compat:453`); jakmile Dream/`_snip_history` změní začátek pole, prefix se invaliduje.
3. **Interakce s kontextem:** vyšší `contextWindowTokens` = méně časté konsolidace = stabilnější cachovaný prefix → argument cachingem podporuje velké okno, ale jen na cachujících presetech. 3. **Interakce s kontextem:** vyšší `contextWindowTokens` = méně časté konsolidace = stabilnější cachovaný prefix → argument cachingem podporuje velké okno, ale jen na cachujících presetech.
@@ -742,3 +788,9 @@ Měřeno přímo proti Ollamě na `nvidia.hell` (stejný endpoint jako nanobot),
Konkrétně (detach deep-research `ollama-cloud-models-research`, 2026-06-07): web_fetch velké stránky se perzistoval do souboru, parsování přes `exec` blokoval `restrictToWorkspace` guard, a minimax-m3 místo aby přesunul soubor / použil `read_file` (guard to doslova radil) **opakoval identický blokovaný příkaz s kosmetickými obměnami**, prokládal ho triviálními `print('ok')` sanity-checky (četl failure jako rozbitý interpreter) a jednou vystřelil 10× tentýž grep v jednom tahu → **spálil všech 200 `maxToolIterations` bez výsledku**. Stejný úkol s `kimi` doběhl za ~456 s. Konkrétně (detach deep-research `ollama-cloud-models-research`, 2026-06-07): web_fetch velké stránky se perzistoval do souboru, parsování přes `exec` blokoval `restrictToWorkspace` guard, a minimax-m3 místo aby přesunul soubor / použil `read_file` (guard to doslova radil) **opakoval identický blokovaný příkaz s kosmetickými obměnami**, prokládal ho triviálními `print('ok')` sanity-checky (četl failure jako rozbitý interpreter) a jednou vystřelil 10× tentýž grep v jednom tahu → **spálil všech 200 `maxToolIterations` bez výsledku**. Stejný úkol s `kimi` doběhl za ~456 s.
K tomu už dřív známé: tool-result bug + výrazná pomalost. **Zkouší se náhrada `minimax-m2.7`** (starší MiniMax). Pro background deep-research drž GLM-5.1 / Kimi, ne MiniMax. Plný rozbor smyčky: session `detach_2026-06-07T170344-ollama-cloud-models-research.jsonl`. K tomu už dřív známé: tool-result bug + výrazná pomalost. **Zkouší se náhrada `minimax-m2.7`** (starší MiniMax). Pro background deep-research drž GLM-5.1 / Kimi, ne MiniMax. Plný rozbor smyčky: session `detach_2026-06-07T170344-ollama-cloud-models-research.jsonl`.
## /remind: model nevypisuje celou tabulku → mezera v instrukcích, ne bug skriptu
**Problém → příčina → fix:** Modely (kimi27 i default) po `/remind list` dostanou kompletní tabulku (CLI exit 0), ale jeden ji scvrkne na počet, druhý si ji přerenderuje po svém. → Příčina: `SKILL.md` sekce `## Behavioral contract` *popisovala formát* výstupu, ale neříkala „předej kompletní" — modely to čtou jako surová data k vlastnímu formátování. → Fix: odstavec **Showing read results** (`list`/`upcoming`/`delivered` = user-ready text, vypsat každou položku s `#display-id`, nesumarizovat). Plný záznam: history 2026-06-15 13:21.
**Diagnostika chování nanobot agenta = číst webui session logy.** Konverzace (user/assistant/tool turny, včetně `reasoning_content` a `exec` výstupů) žijí na serveru ve `workspace/sessions/websocket_<id>.jsonl` (kopie i v `~/.nanobot/webui/`). Pro „proč model udělal X" stáhnout příslušnou session a číst turny — odhalí, že příkaz uspěl a chyba je až v prezentaci.

View File

@@ -1,3 +1,5 @@
- Podcast "Máš na míň" — moderátor Vašek Matějovský spolumoderuje s Radarem (Michal Vrátný, zakladatel Železné koule) - Podcast "Máš na míň" — moderátor Vašek Matějovský spolumoderuje s Radarem (Michal Vrátný, zakladatel Železné koule)
- Chce mít filmy uložené lokálně na disku — streaming služby mění podmínky a dostupnost, chce mít obsah trvale k dispozici po zaplacení
- Při studiu 3+ souborů z git repozitáře (typicky GitHub) naklonuj do tmp/ a zkoumej lokálně — ale jen pokud repo není obrovské (desítky MB OK, stovky MB+ už ne) - Při studiu 3+ souborů z git repozitáře (typicky GitHub) naklonuj do tmp/ a zkoumej lokálně — ale jen pokud repo není obrovské (desítky MB OK, stovky MB+ už ne)
- Otestovat s Claude Code plugin llm-wiki-plugin (https://github.com/praneybehl/llm-wiki-plugin) - Otestovat s Claude Code plugin llm-wiki-plugin (https://github.com/praneybehl/llm-wiki-plugin)
- Vyzkoušet LibreChat

View File

@@ -1 +1 @@
379 409

View File

@@ -377,3 +377,33 @@
{"cursor": 377, "timestamp": "2026-06-29 16:50", "content": "- [permanent] User communicates in Czech; respond in Czech.\n- [durable] User has a child named Ema.\n- [durable] User has someone named Bětka (likely a partner/child) for whom they handle paperwork.\n- [durable] User's servers: wood.hell, pivo.hell.\n- [durable] User uses Zabbix for monitoring; has a \"giga\" device to integrate into it.\n- [durable] User goes to Silver Gym.\n- [durable] User has a space/room called \"chlívek\" with a window (pigeon spikes needed) and a temperature sensor project.", "session_key": "telegram:8826147089"} {"cursor": 377, "timestamp": "2026-06-29 16:50", "content": "- [permanent] User communicates in Czech; respond in Czech.\n- [durable] User has a child named Ema.\n- [durable] User has someone named Bětka (likely a partner/child) for whom they handle paperwork.\n- [durable] User's servers: wood.hell, pivo.hell.\n- [durable] User uses Zabbix for monitoring; has a \"giga\" device to integrate into it.\n- [durable] User goes to Silver Gym.\n- [durable] User has a space/room called \"chlívek\" with a window (pigeon spikes needed) and a temperature sensor project.", "session_key": "telegram:8826147089"}
{"cursor": 378, "timestamp": "2026-06-29 20:58", "content": "- [skip] User queried what was stored in memory over the last 4 days — informational, no new facts\n- [skip] User asked if all history entries were propagated to MEMORY.md — informational query\n- [skip] User asked about compact-memory skill — informational query\n- [durable] Dream's routing rules (SOUL.md → personality, USER.md → user profile, MEMORY.md → project knowledge) are hardcoded in the Dream prompt and not user-configurable beyond interval/model/batch size\n- [durable] Dream Phase 1 does dedup detection across memory files; Phase 2 does surgical line-level edits, never full file rewrites\n- [durable] MEMORY.md lines older than 14 days get age annotations (e.g. `← 37d`) injected only into Dream's prompt, not written to disk", "session_key": "telegram:8826147089"} {"cursor": 378, "timestamp": "2026-06-29 20:58", "content": "- [skip] User queried what was stored in memory over the last 4 days — informational, no new facts\n- [skip] User asked if all history entries were propagated to MEMORY.md — informational query\n- [skip] User asked about compact-memory skill — informational query\n- [durable] Dream's routing rules (SOUL.md → personality, USER.md → user profile, MEMORY.md → project knowledge) are hardcoded in the Dream prompt and not user-configurable beyond interval/model/batch size\n- [durable] Dream Phase 1 does dedup detection across memory files; Phase 2 does surgical line-level edits, never full file rewrites\n- [durable] MEMORY.md lines older than 14 days get age annotations (e.g. `← 37d`) injected only into Dream's prompt, not written to disk", "session_key": "telegram:8826147089"}
{"cursor": 379, "timestamp": "2026-06-30 05:19", "content": "- [durable] Dream pravidla (Phase 1 + Phase 2 instrukce, fact/decision/ephemeral klasifikace, dedup logika) jsou hardcoded v Python zdrojovém kódu nanobotu, ne v konfiguračních souborech\n- [durable] Konfigurovatelné Dream nastavení je omezené na: intervalH, modelOverride, maxBatchSize, maxIterations\n- [durable] Workspace safety guard omezuje file tooly na ~/.nanobot/workspace/ — cesty mimo něj jsou nepřístupné i pro čtení zdrojového kódu nanobotu\n- [durable] Nanobot zdrojový kód je instalován z /home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packages/nanobot/", "session_key": "telegram:8826147089"} {"cursor": 379, "timestamp": "2026-06-30 05:19", "content": "- [durable] Dream pravidla (Phase 1 + Phase 2 instrukce, fact/decision/ephemeral klasifikace, dedup logika) jsou hardcoded v Python zdrojovém kódu nanobotu, ne v konfiguračních souborech\n- [durable] Konfigurovatelné Dream nastavení je omezené na: intervalH, modelOverride, maxBatchSize, maxIterations\n- [durable] Workspace safety guard omezuje file tooly na ~/.nanobot/workspace/ — cesty mimo něj jsou nepřístupné i pro čtení zdrojového kódu nanobotu\n- [durable] Nanobot zdrojový kód je instalován z /home/nanobot/.local/share/uv/tools/nanobot-ai/lib/python3.13/site-packages/nanobot/", "session_key": "telegram:8826147089"}
{"cursor": 380, "timestamp": "2026-06-30 06:11", "content": "(nothing)", "session_key": "telegram:8826147089"}
{"cursor": 381, "timestamp": "2026-06-30 06:22", "content": "- [durable] compact-memory-auto-daily cron job recreated in Telegram session (id: 69484aa6) so reports deliver to Telegram instead of the original WebUI session", "session_key": "telegram:8826147089"}
{"cursor": 382, "timestamp": "2026-06-30 13:36", "content": "(nothing)", "session_key": "websocket:d7133f9e-e6cf-48de-a603-bde433d5cd35"}
{"cursor": 383, "timestamp": "2026-07-01 11:44", "content": "- [permanent] User owns a chata (cottage)\n- [durable] Chair-to-desk distance: 25.5 cm\n- [durable] Goal: interconnect remind, keep, note, and todo into a unified workflow\n- [ephemeral] Interested in DT Glass upcycled wine-bottle glasses (UNIVERSAL and Amber lines, from darkove-sklo.com)\n- [ephemeral] Shopping list for chata: water canister, kitchen double socket, 2× Coca-Cola glasses, small Pepsi glass, tea strainer/teapot, tea cup", "session_key": "websocket:92e5868b-22c3-4be5-a34d-897d83b983dc"}
{"cursor": 384, "timestamp": "2026-07-01 20:15", "content": "- [durable] Prefer running Claude Code on devlin.hell over WSL on notebook — notebook overheats fan and creates load; devlin.hell allows connecting from multiple locations and resuming work; tmux is an advantage\n- [ephemeral] Current project focus is nanobot, doing experiments there", "session_key": "websocket:90609ec0-901d-4f23-bf37-24abbdba8842"}
{"cursor": 385, "timestamp": "2026-07-02 15:04", "content": "- [permanent] User communicates in Czech and reads Czech-language tech sites (Root.cz, PCTuning.cz, vtm.zive.cz, hackaday.com)\n- [durable] User has technical interests in: Linux sysadmin/boot (EFIStub, GRUB), AI security (prompt injection, CoT forgery), ESP32 hardware, Claude Code tooling\n- [ephemeral] User switched model preset to kimi27 (kimi-k2.7-code:cloud)", "session_key": "websocket:5e925bb0-9841-4635-a4a4-d47ef4cd34f8"}
{"cursor": 386, "timestamp": "2026-07-02 15:26", "content": "- [permanent] User communicates in Czech\n- [durable] PCTuning.cz articles cannot be fetched via Jina extractor (returns only logo SVG) — likely behind paywall or requires JS rendering", "session_key": "websocket:5e925bb0-9841-4635-a4a4-d47ef4cd34f8"}
{"cursor": 387, "timestamp": "2026-07-02 15:32", "content": "- [permanent] User communicates in Czech", "session_key": "websocket:ad87c73a-7c25-466c-a473-52526c120a58"}
{"cursor": 388, "timestamp": "2026-07-02 15:34", "content": "- [permanent] User communicates in Czech", "session_key": "websocket:5e925bb0-9841-4635-a4a4-d47ef4cd34f8"}
{"cursor": 389, "timestamp": "2026-07-02 15:35", "content": "(nothing)", "session_key": "websocket:5e925bb0-9841-4635-a4a4-d47ef4cd34f8"}
{"cursor": 390, "timestamp": "2026-07-02 17:06", "content": "- [durable] User communicates in Czech; prefers Czech responses\n- [durable] Bookmark skill has no delete/remove command — only add, list, read, unread, show, content, history\n- [durable] vtm.zive.cz and zive.cz block automated fetching (Cloudflare bot protection); full article content requires manual HTML paste from user\n- [ephemeral] Re-added Microsoft Intelligent Terminal article as new bookmark #1 with full content; old duplicate #10 marked as read", "session_key": "websocket:dde07759-d885-429e-821f-fb3725f72b93"}
{"cursor": 391, "timestamp": "2026-07-03 16:59", "content": "(nothing)", "session_key": "websocket:b49d7340-9640-4d5f-945d-dc8a7430e9a7"}
{"cursor": 392, "timestamp": "2026-07-05 13:26", "content": "- [skip] Switched model preset to kimi (kimi-k2.6:cloud)", "session_key": "telegram:8826147089"}
{"cursor": 393, "timestamp": "2026-07-05 13:26", "content": "- [permanent] Coding behavioral guidelines: don't assume (surface tradeoffs, ask if ambiguous), minimum code (no speculative/patterns), touch only what you must (surgical changes, clean own mess), define success criteria (loop until verified).\n- [durable] AGENTS.md is for agent work instructions and decision-making rules; SOUL.md is for communication style and personality.", "session_key": "telegram:8826147089"}
{"cursor": 394, "timestamp": "2026-07-05 13:27", "content": "(nothing)", "session_key": "telegram:8826147089"}
{"cursor": 395, "timestamp": "2026-07-05 19:47", "content": "- [durable] User prefers new content/sections to be appended at the end of files rather than inserted in the middle", "session_key": "telegram:8826147089"}
{"cursor": 396, "timestamp": "2026-07-06 07:24", "content": "- [permanent] User prefers that reminders with multiple times are created as a single reminder with multiple schedule entries (e.g., `add --at T1 --at T2`), not as separate reminders for each time", "session_key": "telegram:8826147089"}
{"cursor": 397, "timestamp": "2026-07-09 09:07", "content": "- [ephemeral] Model preset switched to `kimi27` (`kimi-k2.7-code:cloud`, 262144 context window, 16384 max output tokens).", "session_key": "websocket:77ddd845-cb12-44f0-aae9-68f420c88342"}
{"cursor": 398, "timestamp": "2026-07-09 09:54", "content": "- [skip] User switched model preset to `glm`", "session_key": "websocket:2d7e489d-4f68-4932-843f-0b41443b88e7"}
{"cursor": 399, "timestamp": "2026-07-09 10:16", "content": "- [durable] User plans trip to Panama (Panama City, PTY) from Prague (PRG) for Christmas 2026 — departing ~21 Dec 2026, returning ~4 Jan 2027, 3 passengers, minimum 2 weeks stay, flexible on dates if cheaper options exist\n- [durable] User communicates in Czech and prefers Czech-language sources and CZK pricing\n- [durable] User values thorough research with source-by-source analysis and lessons-learned for future searches", "session_key": "websocket:2d7e489d-4f68-4932-843f-0b41443b88e7"}
{"cursor": 400, "timestamp": "2026-07-09 10:33", "content": "- [permanent] User prefers to be asked questions one at a time, not all at once\n- [durable] Flight search skill uses only KAYAK as data source (cz.kayak.com domain for CZK prices)\n- [durable] Flight search: max 1 layover, show layover time; output top 35 offers in table (airline, route, duration, layover, price in CZK)\n- [durable] Flight search: store all found results in DB for later querying, not just best offers\n- [durable] Flight search: store price in original currency plus always convert to CZK\n- [durable] Flight search: flex dates ±3 days, but departure+return combinations must respect minimum stay duration\n- [durable] KAYAK URL pattern: use cz.kayak.com for Czech locale/CZK; fs=stops=-2 filters to max 1 layover\n- [ephemeral] Flight-search skill implemented at skills/flight-search/ with CLI script and SQLite DB; cron integration deferred, schema ready for future watchers/alerts", "session_key": "websocket:2d7e489d-4f68-4932-843f-0b41443b88e7"}
{"cursor": 401, "timestamp": "2026-07-13 08:01", "content": "- [permanent] User communicates in Czech; respond in Czech.\n- [durable] nanobot is installed via `uv tool` as the `nanobot-ai` package, with the `nanobot` executable at `/home/nanobot/.local/bin/nanobot`.\n- [ephemeral] Installed version is `nanobot-ai` 0.2.1; latest PyPI release is 0.2.2.\n- [durable] Upgrade this installation with `uv tool upgrade nanobot-ai`; clean reinstall fallback is `uv tool uninstall nanobot-ai` then `uv tool install nanobot-ai`.\n- [durable] If this deployment is containerized, persistent upgrades require updating the container image and redeploying; an in-container `uv tool upgrade` is lost on restart.", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}
{"cursor": 402, "timestamp": "2026-07-13 15:59", "content": "- [durable] User has an active weekly random reminder to print a notebook holder for the Losiny cottage, firing once per week at a random time between 8:00 and 21:00.\n- [durable] The remind skill does not natively support random reminders with periods longer than one week (no fortnightly/biweekly option); only `--random-times-per-day` and `--random-times-per-week` are available.", "session_key": "telegram:8826147089"}
{"cursor": 403, "timestamp": "2026-07-16 07:12", "content": "- [ephemeral] Aktivní připomínka ID 49: \"auto brzdy, auto olej, zadní brzdové světlo, klimatizace, žumpa na chatě, palivové dřevo\" — každý den 1× v náhodný čas 8:0021:00, od 14. 7. do 27. 7. 2026.\n- [durable] Skill `compact-memory` nemá spustitelnou implementaci; ve složce je jen `SKILL.md`.\n- [durable] Cron job `compact-memory-auto-daily` selhává, protože volá neexistující skript `skills/compact-memory/scripts/compact_memory.py`.\n- [durable] Git historie `skills/compact-memory/` obsahuje pouze `SKILL.md`; skript `scripts/compact_memory.py` v ní nikdy nebyl.", "session_key": "telegram:8826147089"}
{"cursor": 404, "timestamp": "2026-07-16 08:00", "content": "- [durable] When the `nanobot-version-check` cron detects a new `nanobot-ai` version, its notification now includes the upgrade command `uv tool upgrade nanobot-ai`.\n- [correction] The `nanobot-version-check` cron job ID changed from `42a84295` to `e81cda77`.", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}
{"cursor": 405, "timestamp": "2026-07-18 08:00", "content": "- [durable] Cron job `nanobot-version-check` (schedule `0 8 * * *` Europe/Prague) was recreated to send Telegram notifications with current version, new version, and upgrade command `uv tool upgrade nanobot-ai` when a new version is found.", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}
{"cursor": 406, "timestamp": "2026-07-19 08:00", "content": "- [durable] The cron tool only supports `add`, `list`, `remove` actions — no `update`/`edit`, so modifying a cron job message requires delete + create.\n- [ephemeral] A recurring cron job \"nanobot-version-check\" checks PyPI vs installed `nanobot-ai` version and notifies via Telegram if a newer version exists; upgrade command is `uv tool upgrade nanobot-ai`.\n- [skip] nanobot-ai v0.2.2 was current on both installed and PyPI as of 2025-06-25.", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}
{"cursor": 407, "timestamp": "2026-07-20 08:00", "content": "(nothing)", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}
{"cursor": 408, "timestamp": "2026-07-21 08:00", "content": "(skip)", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}
{"cursor": 409, "timestamp": "2026-07-22 08:00", "content": "(nothing)", "session_key": "websocket:52d0f338-7a06-42e7-aad2-86a4cabbfb9f"}

View File

@@ -0,0 +1,150 @@
# flight-search skill
## Kontext
User pravidelně hledá letenky (např. PRG→PTY na Vánoce 2026). Chce automatizovaný skill, který vyhledává letenky přes KAYAK, ukládá výsledky do SQLite a umožňuje dotazování nad historii. Později se napojí na cron pro pravidelné hledání a alertování.
## Rozhodnutí z plánovací fáze
- **Zdroj dat**: Jen KAYAK (spolehlivě funguje přes Jina extractor)
- **Výstup**: Top 35 nabídek v tabulce (aerolinka, trasa, cena, doba letu, přestup)
- **Layover**: Max 1 přestup, v tabulce uvést čas přestupu
- **DB**: Uložit vše — definice hledání i nalezené výsledky, původní měnu + CZK
- **Cron**: Neimplementovat teď, ale DB schema připravit pro rozšíření
- **Měna**: Vždy hledat s `curr=CZK`, uložit `price_czk`; pokud je viditelná původní měna, uložit i tu
- **Flex data**: ±3 dny, ale kombinace odlet+návrat musí splňovat `min_stay_days`
## Postup
### 1. Vytvořit adresářovou strukturu skillu
```
skills/flight-search/
├── SKILL.md
└── scripts/
└── flight_search.py
```
DB se auto-vytvoří v `db/flight_search.sqlite`.
### 2. DB schema
```sql
CREATE TABLE searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
origin TEXT NOT NULL, -- IATA kód (PRG)
destination TEXT NOT NULL, -- IATA kód (PTY)
dep_date TEXT NOT NULL, -- ISO date (2026-12-21)
ret_date TEXT NOT NULL, -- ISO date (2027-01-04)
adults INTEGER NOT NULL DEFAULT 1,
max_layovers INTEGER NOT NULL DEFAULT 1,
min_stay_days INTEGER NOT NULL DEFAULT 1,
flex_days INTEGER NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'CZK',
status TEXT NOT NULL DEFAULT 'active', -- active, completed, failed
created_at TEXT NOT NULL,
completed_at TEXT
);
CREATE TABLE results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
search_id INTEGER NOT NULL REFERENCES searches(id),
airline TEXT NOT NULL,
route TEXT NOT NULL, -- "PRG→AMS→PTY / PTY→AMS→PRG"
dep_date TEXT NOT NULL, -- skutečný datum odletu (může se lišit od search.dep_date při flex)
ret_date TEXT NOT NULL,
dep_time TEXT, -- čas odletu
arr_time TEXT, -- čas příletu
layovers INTEGER NOT NULL DEFAULT 0,
layover_info TEXT, -- "AMS 2h 15m"
duration TEXT, -- celková doba letu "12h 30m"
price REAL, -- cena v původní měně
price_currency TEXT NOT NULL DEFAULT 'CZK',
price_czk REAL NOT NULL, -- cena v CZK
booking_url TEXT,
found_at TEXT NOT NULL
);
CREATE INDEX idx_results_search ON results(search_id);
CREATE INDEX idx_results_route ON results(route);
```
### 3. CLI skript `flight_search.py`
Podpříkazy:
| Příkaz | Popis |
|--------|-------|
| `create-search` | Vytvoří záznam hledání, vrátí `search_id` + seznam KAYAK URL k fetchovat |
| `add-result` | Přidá jeden výsledek k hledání |
| `add-results` | Přidá víc výsledků najednou (JSON ze stdin) |
| `results <search_id>` | Vypíše top N výsledků seřazených podle ceny |
| `list-searches` | Seznam všech hledání |
| `delete-search <id>` | Smaže hledání + výsledky |
**`create-search`** generuje KAYAK URL:
- Základní URL: `https://www.kayak.com/flights/{origin}-{destination}/{dep_date}/{ret_date}/{adults}adults?sort=price_a&curr={currency}`
- S flex dny: generuje všechny platné kombinace (dep ± flex, ret ± flex, ret - dep >= min_stay_days)
- Max 15 URL (cap na rozumný počet requestů)
- Strategie: středový datum pár první, pak expanduje
- Výstup: JSON s `search_id` a `urls` pole
**`add-result`** přijímá parametry jako CLI flagy:
```
flight_search.py add-result --search-id 1 --airline "KLM" --route "PRG→AMS→PTY" ...
```
**`results`** formátuje tabulku:
```
#1 KLM PRG→AMS→PTY / PTY→AMS→PRG 12h30m 1 stop (AMS 2h15m) 15 420 CZK
#2 TAP PRG→LIS→PTY / PTY→LIS→PRG 14h10m 1 stop (LIS 1h45m) 16 200 CZK
```
### 4. SKILL.md
Skill je **agent-driven** (jako deep-research). Agent:
1. Získá parametry od uživatele (origin, destination, dates, adults, flex, min_stay, max_layovers)
2. Zavolá `create-search` → získá search_id a seznam URL
3. Pro každou URL zavolá `web_fetch`
4. Z fetched markdown parsuje letové výsledky (aerolinka, trasa, časy, cena, přestupy)
5. Uloží výsledky přes `add-result`
6. Zavolá `results <search_id>` a prezentuje uživateli top 35
SKILL.md obsahuje:
- Kdy se skill aktivuje ("hledej letenky", "flight search", "letenky do X")
- Postup orchestration (krok za krokem)
- KAYAK URL konstrukce (včetně flex data strategie)
- Parsing guidance (co hledat v markdown výstupu KAYAKu)
- Formátování výstupu pro uživatele
- Command reference pro `flight_search.py`
### 5. KAYAK URL parametry k ověření
Při implementaci ověřit:
- Filter na max 1 přestup: pravděpodobně `fs=stops=0` (0 additional stops), ale nutno testovat
- Flex dates: zda KAYAK má nativní flex parameter (např. `flexible_dates=3`), což by snížilo počet requestů
- Pokud nativní flex nefunguje, použít strategii generování kombinací z bodu 3
### 6. CZK přepočet
- KAYAK URL vždy s `curr=CZK` → ceny rovnou v CZK
- `price_czk` = cena z KAYAKu
- `price_currency` = CZK
- Pokud by se později přidaly zdroje v jiné měně, přidat exchange rate fetch (např. z ČNB API)
### 7. Cron příprava (neimplementovat)
DB schema je připraveno pro rozšíření:
- Přidat tabulku `watchers` s threshold cenou a notifikačním kanálem
- Přidat flag `is_watched` do `searches`
- Cron job by volal `create-search` + fetch + `add-result` + kontrola threshold
## Ověření
1. Vytvořit skill adresář a skript
2. Spustit `create-search --origin PRG --destination PTY --dep 2026-12-21 --ret 2027-01-04 --adults 3 --flex 3 --min-stay 14 --max-layovers 1`
3. Ověřit, že generované URL jsou platné KAYAK odkazy
4. Fetchnout jednu URL přes `web_fetch` a ověřit, že vrací letové výsledky
5. Uložit výsledek přes `add-result` a ověřit v DB
6. Zavolat `results <search_id>` a ověřit formátování
7. Ověřit, že flex date kombinace respektují min_stay constraint

290
scripts/benchmark_ollama.py Normal file
View File

@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""Benchmark Ollama cloud models: TTFT, throughput, thinking vs response phases.
Measures:
- TTFT-thinking: time to first thinking token
- TTFT-response: time to first response token (visible output)
- Thinking phase: duration and tok/s of reasoning
- Response phase: duration and tok/s of visible output
- Total wall time and overall tok/s
- Token counts (prompt, thinking, response, total eval)
Usage:
python3 benchmark_ollama.py [--host nvidia.hell] [--port 11434]
python3 benchmark_ollama.py --models glm-5.1,glm-5.2,kimi-k2.6 --runs 3
python3 benchmark_ollama.py --list
"""
import argparse
import json
import time
import sys
import urllib.request
import urllib.error
DEFAULT_PROMPT = "Write a detailed 500-word essay about the history of computing, from Babbage to modern AI. Include key milestones, people, and technologies."
DEFAULT_HOST = "nvidia.hell"
DEFAULT_PORT = 11434
DEFAULT_NUM_PREDICT = 1000
def api_get(host, port, path):
url = f"http://{host}:{port}{path}"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
def api_post_stream(host, port, path, payload):
"""POST with streaming response. Yields (line_dict, elapsed_since_start)."""
url = f"http://{host}:{port}{path}"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url, data=data, method="POST",
headers={"Content-Type": "application/json"},
)
t0 = time.monotonic()
resp = urllib.request.urlopen(req, timeout=300)
buffer = b""
while True:
chunk = resp.read(1)
if not chunk:
break
buffer += chunk
if chunk == b"\n":
line = buffer.strip()
buffer = b""
if not line:
continue
try:
obj = json.loads(line)
yield obj, time.monotonic() - t0
except json.JSONDecodeError:
continue
def list_models(host, port):
try:
data = api_get(host, port, "/api/tags")
return [m["name"] for m in data.get("models", [])]
except Exception as e:
print(f"Error listing models: {e}", file=sys.stderr)
return []
def benchmark_model(host, port, model, prompt, num_predict):
"""Run a single benchmark against one model."""
payload = {
"model": model,
"prompt": prompt,
"stream": True,
"options": {"temperature": 0.0, "num_predict": num_predict},
}
# Track timing for thinking and response phases
first_thinking_time = None
last_thinking_time = None
first_response_time = None
last_response_time = None
thinking_chars = 0
response_chars = 0
# Stats from final chunk
prompt_eval_count = 0
eval_count = 0
total_duration_ns = 0
done_reason = ""
try:
for obj, elapsed in api_post_stream(host, port, "/api/generate", payload):
if obj.get("error"):
return {"model": model, "error": obj["error"]}
thinking = obj.get("thinking", "") or ""
response = obj.get("response", "") or ""
if thinking:
if first_thinking_time is None:
first_thinking_time = elapsed
last_thinking_time = elapsed
thinking_chars += len(thinking)
if response:
if first_response_time is None:
first_response_time = elapsed
last_response_time = elapsed
response_chars += len(response)
if obj.get("done"):
prompt_eval_count = obj.get("prompt_eval_count", 0)
eval_count = obj.get("eval_count", 0)
total_duration_ns = obj.get("total_duration", 0)
done_reason = obj.get("done_reason", "")
break
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:200]
return {"model": model, "error": f"HTTP {e.code}: {body}"}
except Exception as e:
return {"model": model, "error": str(e)}
# Calculate metrics
# Estimate token split: thinking vs response by char ratio
total_gen_chars = thinking_chars + response_chars
if total_gen_chars > 0:
thinking_tokens_est = round(eval_count * thinking_chars / total_gen_chars)
response_tokens_est = round(eval_count * response_chars / total_gen_chars)
else:
thinking_tokens_est = 0
response_tokens_est = 0
# Phase durations
thinking_duration = 0
if first_thinking_time and last_thinking_time:
thinking_duration = last_thinking_time - first_thinking_time
response_duration = 0
if first_response_time and last_response_time:
response_duration = last_response_time - first_response_time
# Total generation time (from first token to last token, whether thinking or response)
first_token_time = None
last_token_time = None
if first_thinking_time is not None or first_response_time is not None:
first_token_time = min(
t for t in [first_thinking_time, first_response_time] if t is not None
)
if last_thinking_time is not None or last_response_time is not None:
last_token_time = max(
t for t in [last_thinking_time, last_response_time] if t is not None
)
total_gen_time = 0
if first_token_time and last_token_time:
total_gen_time = last_token_time - first_token_time
# Throughput calculations
# Overall: eval_count / total_gen_time (all tokens including thinking)
overall_tps = (eval_count / total_gen_time) if total_gen_time > 0 else 0
# Thinking phase throughput
thinking_tps = (thinking_tokens_est / thinking_duration) if thinking_duration > 0 else 0
# Response phase throughput
response_tps = (response_tokens_est / response_duration) if response_duration > 0 else 0
# Wall-clock total (from request start to last token)
wall_total = last_token_time or 0
# TTFT metrics
ttft_thinking = first_thinking_time
ttft_response = first_response_time
return {
"model": model,
"done_reason": done_reason,
"ttft_thinking_s": round(ttft_thinking, 3) if ttft_thinking else None,
"ttft_response_s": round(ttft_response, 3) if ttft_response else None,
"wall_total_s": round(wall_total, 3),
"prompt_tokens": prompt_eval_count,
"eval_count": eval_count,
"thinking_tokens_est": thinking_tokens_est,
"response_tokens_est": response_tokens_est,
"thinking_duration_s": round(thinking_duration, 3),
"response_duration_s": round(response_duration, 3),
"thinking_tps": round(thinking_tps, 1),
"response_tps": round(response_tps, 1),
"overall_tps": round(overall_tps, 1),
"thinking_chars": thinking_chars,
"response_chars": response_chars,
}
def main():
parser = argparse.ArgumentParser(description="Benchmark Ollama models (thinking + response)")
parser.add_argument("--host", default=DEFAULT_HOST)
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
parser.add_argument("--models", help="Comma-separated model list (default: all cloud)")
parser.add_argument("--num-predict", type=int, default=DEFAULT_NUM_PREDICT)
parser.add_argument("--runs", type=int, default=1)
parser.add_argument("--list", action="store_true")
args = parser.parse_args()
print(f"Connecting to http://{args.host}:{args.port} ...")
models = list_models(args.host, args.port)
if not models:
print("No models found or connection failed.", file=sys.stderr)
sys.exit(1)
# Filter to cloud models by default for relevance
cloud_models = [m for m in models if ":cloud" in m]
all_models = models
if args.list:
print(f"Available models ({len(models)}):")
for m in all_models:
print(f" - {m}")
return
# Select models
if args.models:
wanted = [m.strip() for m in args.models.split(",")]
selected = []
for w in wanted:
matches = [m for m in all_models if w.lower() in m.lower()]
if matches:
selected.extend(matches)
else:
print(f" Warning: '{w}' not found", file=sys.stderr)
seen = set()
selected = [m for m in selected if not (m in seen or seen.add(m))]
else:
selected = cloud_models
if not selected:
print("No matching models selected.", file=sys.stderr)
sys.exit(1)
print(f"\nBenchmarking {len(selected)} model(s), {args.runs} run(s) each, max {args.num_predict} tokens")
print(f"Prompt: \"{args.prompt[:80]}...\"")
print(f"{'='*120}")
results = []
for model in selected:
for run in range(args.runs):
run_label = f"run {run+1}/{args.runs}" if args.runs > 1 else ""
print(f"\n[{model}] {run_label}")
r = benchmark_model(args.host, args.port, model, args.prompt, args.num_predict)
results.append(r)
if "error" in r:
print(f" ERROR: {r['error']}")
continue
print(f" Done reason: {r['done_reason']}")
tt = f"{r['ttft_thinking_s']:.3f}s" if r['ttft_thinking_s'] else "N/A"
tr = f"{r['ttft_response_s']:.3f}s" if r['ttft_response_s'] else "N/A"
print(f" TTFT thinking: {tt}")
print(f" TTFT response: {tr}")
print(f" Wall total: {r['wall_total_s']:.3f}s")
print(f" Tokens: prompt={r['prompt_tokens']} total_gen={r['eval_count']} (think~{r['thinking_tokens_est']} resp~{r['response_tokens_est']})")
print(f" Think phase: {r['thinking_duration_s']:.3f}s @ {r['thinking_tps']:.1f} tok/s")
print(f" Resp phase: {r['response_duration_s']:.3f}s @ {r['response_tps']:.1f} tok/s")
print(f" Overall tps: {r['overall_tps']:.1f} tok/s ({r['eval_count']} tok in {r['wall_total_s']:.1f}s)")
# Summary table
print(f"\n{'='*120}")
print("SUMMARY")
print(f"{'Model':<22} {'Reason':>8} {'TTFT-t':>7} {'TTFT-r':>7} {'Wall s':>7} {'Tokens':>7} {'Think t':>7} {'Think tps':>9} {'Resp t':>7} {'Resp tps':>9} {'Overall':>8}")
print("-" * 120)
for r in results:
if "error" in r:
print(f"{r['model']:<22} ERROR: {r['error'][:50]}")
continue
tt = f"{r['ttft_thinking_s']:.3f}" if r['ttft_thinking_s'] else " N/A"
tr = f"{r['ttft_response_s']:.3f}" if r['ttft_response_s'] else " N/A"
print(f"{r['model']:<22} {r['done_reason']:>8} {tt:>7} {tr:>7} {r['wall_total_s']:>7.3f} {r['eval_count']:>7} {r['thinking_duration_s']:>7.3f} {r['thinking_tps']:>9.1f} {r['response_duration_s']:>7.3f} {r['response_tps']:>9.1f} {r['overall_tps']:>8.1f}")
if __name__ == "__main__":
main()

71
scripts/remind_add.py Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""One-off helper to add a reminder directly to the SQLite DB."""
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
WORKSPACE = Path(__file__).resolve().parent.parent
DB_PATH = WORKSPACE / "db" / "reminders.sqlite"
def main() -> int:
if len(sys.argv) < 3:
print("usage: remind_add.py <at_iso> <text>", file=sys.stderr)
return 1
at_str = sys.argv[1]
text = sys.argv[2]
try:
datetime.fromisoformat(at_str)
except ValueError as exc:
print(f"invalid --at datetime: {exc}", file=sys.stderr)
return 1
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.executescript(
"""
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL CHECK(text <> ''),
enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)),
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS schedule_at (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
at_datetime TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_reminders_text ON reminders(text);
CREATE INDEX IF NOT EXISTS idx_at_datetime ON schedule_at(reminder_id, at_datetime);
"""
)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
cur = conn.execute(
"INSERT INTO reminders (text, enabled, timezone, created_at, updated_at) VALUES (?, 1, 'Europe/Prague', ?, ?)",
(text, now, now),
)
reminder_id = cur.lastrowid
conn.execute(
"INSERT INTO schedule_at (reminder_id, at_datetime) VALUES (?, ?)",
(reminder_id, at_str),
)
conn.commit()
conn.close()
print(f"added reminder #{reminder_id} at {at_str}: {text}")
return 0
if __name__ == "__main__":
sys.exit(main())