skill and develop

This commit is contained in:
lachtan
2026-08-27 14:18:51 +02:00
parent 8119f780e7
commit f78f667823
4 changed files with 682 additions and 70 deletions

View File

@@ -2173,3 +2173,157 @@ Běh 5 modelů s gate=3:
**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ý.
## 2026-07-15 — sync skillů ze serveru: bookmark autonomně upravený Dreamem
**Cíl:** Uživatel požádal o obecný sync skillů ze serveru do repa (bez upřesnění, který). Podle pravidla v `CLAUDE.md` („server je zdroj pravdy") jsem prošel checksumem všechny lokální skilly proti serveru před jakýmkoli přepisem.
**Co jsem zkusil:** `rsync --dry-run --checksum --itemize-changes` pro každý skill (`article`, `bash`, `bookmark`, `deep-research`, `detach`, `grill-me`, `keep`, `llm-wiki`, `note`, `plan`, `python`; `remind` ověřen zvlášť plným stažením a `diff -rq`).
**Co fungovalo a proč:** `remind` beze změny (server == lokál, potvrzuje předchozí záznam). `detach` a `llm-wiki` hlásily rozdíl jen v mtime (`.f..t......`, bez `c`/`s`) → obsah identický, ignorováno. `bookmark/SKILL.md` měl skutečný obsahový rozdíl (`>fcst......`) — server má autonomní úpravu (pravděpodobně Dream): (1) přehozené pořadí ve formátu výpisu — `<description>` teď před `[<tags>]`, ne za nimi; (2) nová sekce „Known limitations" (chybí `edit`/`update`, `delete` je jen soft-delete, weby za Cloudflare nejdou auto-fetchnout). Stáhl jsem přesně tuto verzi do `skills/bookmark/SKILL.md`, ověřeno `diff` na 100% shodu.
**Co zbývá:** Server != repo u `keep` (SKILL.md) — dry-run to ukázal, ale nebylo součástí zadání, needitoval jsem. Zvážit v příští session. Drobná nekonzistence, kterou sync odhalil a nechal beze změny: příklad výpisu na řádku `#3 [hackaday.com](...) [linux, strace] — lepší strace` v `bookmark/SKILL.md` neodpovídá nově popsanému formátu (description by měl být před tagy) — je to takto i na serveru, nešlo o repo vs. server rozdíl, takže jsem to nepravil bez zeptání.
## 2026-07-15 — compact-memory-auto-daily: diagnóza "podivných hlášek" na Telegramu + migrace na crontab s čerstvou session
**Cíl:** Uživatel hlásil podivné/nesmyslné hlášky na Telegramu z posledních dní; v noci má běžet `compact-memory` z cronu, ale něco je špatně. Nejdřív jen prozkoumat (bez zásahu), pak — po potvrzení, že denní doručení je záměr — opravit kontaminaci kontextu.
**Diagnóza (nainstalovaný `nanobot-ai` v0.2.2):** Cron job `compact-memory-auto-daily` (`jobs.json`, id `69484aa6`) byl `agent_turn` s `payload.session_key = "telegram:8826147089"` — **stejná trvalá session jako běžný Telegram chat** (potvrzeno v `sessions/telegram_8826147089.jsonl`: 75/77 zpráv obyčejná konverzace, jen 2 cron marker). `is_bound_cron_job`/`run_bound_cron_job` (nahrazuje starý "hardcoded reminder preamble + evaluate_response" mechanismus popsaný výše v knowledge.md pro starší verzi) doručuje odpověď **bez jakéhokoli gatingu** — cokoli agent v tahu napíše, jde přímo do Telegramu. Navíc `origin_delivery_context` (kam se doručí) je **nezávislé** na `session_key` (do jaké session/historie se zapíše) — to je klíč k opravě. 2026-07-14 se agent v cron tahu spletl a pokusil se spustit neexistující `scripts/compact_memory.py` (skill ho nikdy neměl mít), napsal do chatu zmatenou zprávu, uživatel ho opravil ve stejné session — a protože session je trvalá, ten omyl kontaminoval i další noci (přesně text, co uživatel citoval jako "podivnou hlášku", je z nočního běhu 2026-07-15 02:00).
**Řešení (schváleno přes plan mode):** Migrace na vzor `/remind`/`detach` — systémový crontab + samostatný skript mimo nanobot `cron/jobs.json`, s **unikátní `session_key` per běh** (nikdy znovu použitý → nikdy žádná akumulace historie), doručení přímo přes Telegram Bot API (mimo nanobot channel pipeline).
**Co jsem udělal:**
- Stáhl `skills/compact-memory/SKILL.md` ze serveru do repa poprvé (skill dosud nebyl trackovaný lokálně) + přidal větu "This skill has no accompanying script — every step below is performed by you, the agent..." — cílená prevence přesně té 07-14 halucinace.
- Nový `skills/compact-memory/scripts/compact_memory_auto.py` (PEP 723, vzor `tasks-daemon.py`/`remind_send.py`): `Nanobot.from_config().run(GOAL, session_key=f"compact-memory-auto:{timestamp}")` s 10min timeoutem, výsledek poslán přímo `urllib` na Telegram Bot API (token/chat_id z `config.json`, stejný `_telegram_config()` vzor).
- Nasazeno `rsync -av skills/compact-memory/` (celý adresář), owner `nanobot:nanobot` ✓.
- Přidán řádek do system crontabu (`0 2 * * * uv run .../compact_memory_auto.py >> log/compact_memory_auto_cron.log`), stávající 3 řádky (remind/wiki-compile/note-compile) nedotčené.
- Odebrán job `69484aa6` z `jobs.json` (in-place Python edit, bez restartu — zbylé 3 joby beze změny).
- Opravena zastaralá věta v serverovém `memory/MEMORY.md` (psala, že job běží v Telegram session — teď už neplatí).
**Co fungovalo a proč:** Ruční test skriptu na serveru proběhl čistě (exit 0): reálný audit MEMORY.md (32 řádků), skutečně smazal 1 duplicitu a sloučil 2 dvojice, zapsal zálohu do `backup/2026-07-15_0541_memory.backup.md` i do `log/memory-clean.log`, report doručen do Telegramu. Ověřeno, že vznikla **nová izolovaná** session `sessions/compact-memory-auto_20260715-054147.jsonl` a `telegram_8826147089.jsonl` zůstal nedotčený (78 řádků beze změny) — potvrzuje, že `session_key` a `origin_channel`/`origin_chat_id` jsou v `nanobot-ai` 0.2.2 opravdu nezávislé, přesně jak ukázala explorace zdrojáku.
**Co zbývá:** Počkat na první reálný noční běh (příští 2:00) a ověřit stejné chování automaticky. Navrhnout uživateli větu do `decisions.md` (čeká na formulaci autora): "Nanobot `agent_turn` cron joby pro neinteraktivní/background úlohy vždy mimo `jobs.json`, přes systémový crontab + přímé `Nanobot.run()` s per-run unikátní `session_key` — stejný vzor jako `/remind` a `detach`." Zvážit doplnění knowledge.md o `is_bound_cron_job`/`origin_delivery_context` mechanismus 0.2.2 (nahrazuje starší popsaný "hardcoded preamble + evaluate_response" — ten je teď verzí-specifický/zastaralý).
**Jak vrátit zpět:** V `jobs.json` přidat job `69484aa6` zpět (zálohovaný obsah je v git historii commitu před touto změnou), odebrat řádek z crontabu (`crontab -e`), smazat `skills/compact-memory/scripts/compact_memory_auto.py`. Migrace `MEMORY.md` (smazaná duplicita, sloučené bullet) je nedestruktivní — plný snapshot je v `backup/2026-07-15_0541_memory.backup.md` na serveru.
## 2026-07-22 — nový skill `project` (nahrazen netrackovaný server-side skill, migrace `radio-1`)
**Cíl:** Uživatel chtěl nový skill `project`, analogii Claude.ai "Projects" — pojmenované dlouhodobé pracovní kontexty s vlastním promptem, historií a artefakty. Plánováno v plan mode: prozkoumáno 13 existujících skillů + `knowledge.md` pro konvence (`description` = jen routing signál s "Triggers on:", cesty workspace-relativní, žádný sticky session-state u nanobota). Uživatel v review odmítl jmenovat konkrétní jiné skilly v description/těle (odlišení jen funkčně) a trval na tom, že skill smí zakládat nový projekt jen po explicitním potvrzení, nikdy automaticky. Přidán čtvrtý soubor `state.md` (živý, přepisovaný dokument "kde to teď je") vedle append-only `memory.md`.
**Co jsem zkusil:** Před nasazením (pravidlo "server je zdroj pravdy") jsem ověřil `~/.nanobot/workspace/skills/project/` na serveru — **a objevil tam už existující, plně funkční `project` skill, nikde v tomto repu netrackovaný** (žádná zmínka v `history.md`/`knowledge.md`/`decisions.md`, žádná git historie k `skills/project/`). Starý design: plochý soubor `projects/<slug>.md` s YAML frontmatterem (`status: active|paused|done`, `priority: high|medium|low`), tělo se sekcemi `## Poznámky`/`## Další krok`, CLI backend `scripts/project.py` (add/list/show/status), `switch` ukládající aktivní projekt do `my(action="set", key="project_context", ...)` scratchpad nástroje, frontmatter měl i `aliases: [proj]`. Existovala reálná data: `projects/radio-1.md` (založeno 2026-06-09, projekt na stříhání audio streamu Radia 1).
**Co fungovalo a proč:** Zastavil jsem se a nechal uživatele rozhodnout (AskUserQuestion) — odpověď: přepsat novým designem, `radio-1` migrovat na `radio1`. Provedeno: `rsync -av --delete skills/project/ nanobot@...` (smazal starý `scripts/project.py`, nahradil `SKILL.md` novým), obsah `radio-1.md` ručně rozparsován do nového formátu — dated bullet z `## Poznámky` → `projects/radio1/memory.md`, cíl+stav+priorita syntetizovány do `projects/radio1/state.md`, `prompt.md` prázdný (nic k migraci), `artifacts/` prázdný adresář — nahráno přes `rsync` do `~/.nanobot/workspace/projects/radio1/`, starý `projects/radio-1.md` smazán. Ověřeno vlastnictví `nanobot:nanobot` u všech nových souborů/adresářů.
**Co zbývá:** Ověřit na živém chatu, že skill funguje end-to-end (aktivace, switch, ukončení, listing, zápis do `memory.md`/`state.md`) — zatím jen nasazeno a data migrována, ne vyzkoušeno v konverzaci. Zvážit zápis do `knowledge.md`: nanobot má `my(action="set"/"get", key=..., value=...)` scratchpad nástroj pro perzistentní stav napříč tahy (starý skill ho používal pro `project_context`) — objeveno náhodou, nebylo součástí dřívějšího průzkumu "nanobot nemá sticky session-state"; stálo by za ověření zdrojáku, jak přesně `my` funguje (per-session, nebo globální — starý skill sám poznamenává "lost on restart", žádnou zmínku o per-session scoping). Navrhnout `decisions.md` zápis (viz níže, čeká na souhlas autora).
**Jak vrátit zpět:** Stará verze skillu (`SKILL.md` + `scripts/project.py`) je stažená v `tmp/server-project-check/` (negitované, lokální). Obsah `radio-1.md` je beze změny zachycen ve stejném adresáři přes `git show`/`cat ~/.nanobot/workspace/projects/radio1/memory.md`+`state.md` (sloučit zpět ručně). Žádná záloha stažené staré verze v gitu — je jen v `tmp/`, které je gitignored; pokud má mít trvalou stopu, je potřeba ji dodatečně zkopírovat mimo `tmp/`.
## 2026-07-22 — nový projekt `life` založen na serveru
**Cíl:** Uživatel chtěl na serveru přes skill `project` založit dlouhodobý projekt `life` — osobní průvodce/deníček k udržitelnému životnímu stylu (cvičení, běh, jídelní návyky, skloubení s rodinou a dítětem), kam si bude průběžně zapisovat úvahy. Primární cíl projektu: změna váhy + dobrá kondice.
**Co jsem zkusil:** Ověřil stav serveru (skill `project` nasazený a identický s repem, existoval jen `radio1`, žádný `life`). V plan mode přes AskUserQuestion vyjasnil scope. Rozhodnutí uživatele: jídlo řešit jako návyky (večerní jedení, sladké), NE počítat kalorie ani zapisovat jídlo (kalorické tabulky má stranou); oblasti navíc = alkohol + protahování/mobilita (dělá po běhu, chce integrovat pravidelněji); cvičení (vlastní váha/kettlebell) i běh zatím nepravidelné; pokrok sleduje denním vážením + vnímanou kondicí a bolestmi; role agenta = průvodce/deníček, přístup se bude zpřesňovat; do `artifacts/` jdou prohřešky a reálné překážky.
**Co fungovalo a proč:** Připravil 4 soubory lokálně ve scratchpadu a nasadil `rsync -av scratchpad/life/ nanobot@nanobot.hell:.../projects/life/` — `prompt.md` (kontext + oblasti + kam co zapisovat + „první session" checklist), `state.md` (skeleton výchozího stavu/cílů), `memory.md` (první záznam se založením), `artifacts/prekazky-a-prohresky.md` (prázdný starter). Ověřeno: strom 4 soubory + adresář, vše `nanobot:nanobot`. Skilly/projekty se čtou bez restartu služby.
**Co zbývá:** Vyzkoušet v živém chatu aktivaci („projekt life" → načtení `prompt.md`). Výchozí čísla (aktuální váha, časový rozpočet, cílová váha/horizont) doplní uživatel v první session — záměrně nevyplněno (skill: nefabrikovat obsah). Projekt žije jen na serveru, do repa se necommituje (stejně jako `radio1`).
---
## 2026-07-25 — compact-memory: zkrácení nočního reportu (jeden řádek při nule) + append-only fix logu
**Cíl:** Noční běh `compact-memory` (crontab `0 2 * * *`) posílal na Telegram 7001300 znaků i v nocích, kdy nic nesmazal. Uživatel: při nule stačí věta „nebylo nic k smazání", při reálné změně stačí *co* bylo smazáno/sloučeno a *proč*. Frekvence zůstává.
**Diagnóza:** Server = repo (`diff` čistý, žádný drift). Poslední 4 noci (07-22…07-25) všechny `deleted: 0, merged: 0` a přesto narativní walkthrough sekce po sekci + statistický code block + „Borderline kept". Čtyři zdroje, všechny předepsané:
1. `SKILL.md` Auto mode krok 4 — „Print a detailed list of changes."
2. `SKILL.md` Output format → Auto mode report — vynucený blok `deleted/merged/unchanged` + sekce `Details:`, kterou agent vyplnil i při nule.
3. `SKILL.md` Rules — „list the borderline items you kept in the report" (vyrobilo dvoubodový seznam 07-25).
4. `GOAL` ve skriptu — žádné omezení výstupu + formulace jako uživatelský příkaz → agent si to vyložil jako explicitní vyvolání („protože jsi explicitně vyvolal skill, dělal jsem full audit" — 3 ze 4 nocí).
**Co jsem zkusil:** `SKILL.md` Auto mode přepsán na dvě výstupní cesty (nula → jedna věta, stop, žádná záloha ani log; změna → záloha + řádek per položku s kategorií a důvodem), zrušeno hlášení borderline-kept a přidán explicitní zákaz narace auditu, `Output format` má teď dva auto tvary. `GOAL` doplněn o „unattended nightly run, not an interactive request" + převeden z implicitní konkatenace literálů na triple-quoted string (precedens `SCHEMA = """…"""` v `bookmark.py`/`db.py`). Interactive mode nedotčen. Nasazeno `rsync -av skills/compact-memory/` (celý adresář), owner ✓, bez restartu.
**Co fungovalo a proč:** Tři reálné běhy na serveru (exit 0):
- **Nula změn:** doručeno 56 znaků (`Memory compact: nothing to delete (MEMORY.md, 28 lines).`), žádná nová záloha, log nedotčen, `MEMORY.md` beze změny.
- **Se změnou** (reverzibilní test — vložena zjevně ephemeral odrážka do `MEMORY.md`): doručeno 165207 znaků = hlavička + jeden řádek `[ephemeral] "…" — důvod`, smazána právě ta odrážka, vznikla záloha `backup/2026-07-25_0640_memory.backup.md`, `MEMORY.md` se vrátil do původního stavu (`diff` vs. pretest kopie čistý až na doplněný trailing newline).
**Vedlejší nález a fix — agent přepisoval `memory-clean.log`:** V prvním testu změnové cesty log po zápisu **zmenšil** (246 → 196 B) — starý řádek z 07-19 zmizel. Tohle je stejná ztráta, jaká už 07-15 spolkla zápisy o reálném mazání; prózové „append-only" nestačí, když má agent `write_file`, a moje nová formulace („log obsahuje jen smazané položky") ho ještě pozvala nevyhovující řádek „uklidit". Fix: `SKILL.md` teď předepisuje **deterministický append přes `exec`** (`printf '%s\n' '…' >> log/memory-clean.log`), zakazuje `write_file`/`edit_file` na log a explicitně zakazuje mazat/reformátovat existující řádky, i když neodpovídají aktuálnímu formátu. Po redeployi třetí běh: 196 → 350 B, starý řádek zůstal ✓. (Oba záznamy skončily na jednom řádku, protože předchozí zápis neměl trailing newline — soubor teď newline má, takže se to samo srovná.)
**Co zbývá:** Zkontrolovat noční běh 2026-07-26 v `log/compact_memory_auto_cron.log` (řádek `Response to cli:user:` má být krátký).
**Jak vrátit zpět:** `git checkout ef0d6cc~1 -- skills/compact-memory/` + `rsync -av skills/compact-memory/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/compact-memory/`. Testovací zásahy do `MEMORY.md` jsou už vrácené (agent smazal právě ty vložené odrážky); zálohy z běhů jsou v `backup/2026-07-25_06*`.
## 2026-07-26 — compact-memory: model navrhuje, skript validuje a aplikuje (konec prompt-compliance)
**Cíl:** Noční běh 2026-07-26 02:00 poslal na Telegram 1737 znaků narace („Now I have all four files. Let me audit MEMORY.md systematically…"), přestože včerejší oprava přesně tohle zakazovala. Uzavírá položku „zkontrolovat noční běh 07-26" z předchozího záznamu — a odpovídá na ni negativně.
**Diagnóza:** Skill na serveru identický s repem (`diff -ru` → IDENTICAL), takže žádný drift. V session `compact-memory-auto_20260726-020001.jsonl` je finální assistant zpráva 1737 znaků: celá úvaha jako text, na konci správný jednořádkový report. Skript posílal `result.content` beze změny → doručilo se všechno. Srovnání s včerejšími testy (56 a 165 znaků, 3 iterace, dávkové tool cally) vs. dnešek (8 iterací po jednom volání, soubory čtené přes `grep pattern="."`): stejný skill, stejný `GOAL`, stejný model `glm-5.1:cloud`, jiné chování. Závěr: formát výstupu nesmí viset na prompt-compliance, 3 čisté testy nedokazují nic.
**Co jsem zkusil (návrh schválen v plan mode, plán `~/.claude/plans/dobre-pojme-vylepsit-ten-buzzing-quasar.md`):** Obrácení zodpovědností — agent v nočním režimu **needituje nic** a vrací jen JSON change-set (`{"changes": [{op, category, original, new_text, reason}]}`), skript ho validuje, aplikuje, zálohuje, loguje a **sám skládá českou zprávu**. Klíčové volby: `original`/`new_text` jsou **pole řádků** (ne string s `\n` — tam modely kazí escapování); položky se identifikují **doslovným textem, ne čísly řádků**, a blok se musí v souboru vyskytovat právě jednou (halucinovaný citát spadne na validaci místo aby smazal špatný řádek); cílový soubor je **konstanta ve skriptu**, ne pole v JSON (uživatelova připomínka: pole `file` je zbytečná ceremonie, když je whitelist jednoprvkový); limity (`reason` ≤ 120 znaků, `new_text` ≤ 3 řádky/300 znaků, `original` ≤ 20 řádků, smazání < 50 % souboru) jsou **chyba, ne oříznutí**; nevalidní výstup se až **3× vrací modelu** s konkrétním seznamem chyb ve stejné (jednorázové) session; **SHA-256 guard** pozná, kdyby agent soubor upravil sám — agent má `write_file`/`exec` pořád k dispozici, takže zákaz v promptu je jen prosba. Noční běh navíc přepnut z `glm` na preset **`kimi27`** (`kimi-k2.7-code:cloud`) přes `Nanobot.from_config(model_preset=…)` — dopad omezen na tento job. Tvrdá selhání (timeout, výjimka) nově taky posílají jednořádkovou hlášku, dřív skončila tiše exit kódem 1.
**Co fungovalo a proč:**
- **Offline testy parseru** (30 kontrol, `scratchpad/test_parser.py`, stub modulu `nanobot` + `importlib`): validní delete/merge, prázdný change-set, bare JSON bez fence, JSON obalený narací, `reason` přes limit, citát co v souboru není, citát vyskytující se 11×, neznámé pole, špatná kategorie/`op`, merge co nezkracuje, překryv bloků, smazání 19 z 28 řádků, sběr chyb ze všech položek najednou — plus aplikace na kopii (záloha, append-only log, přestavěný soubor) a formát reportu. Vše zelené.
- **Test 1 (nic k smazání):** jeden pokus, model vrátil přesně `{"changes": []}`, exit 0, hash `MEMORY.md` beze změny, log 350 B beze změny, žádná nová záloha.
- **Test 2 (reverzibilní změna):** vložena ephemeral odrážka → model ji jako jedinou navrhl smazat, skript aplikoval, `diff` proti pretest kopii čistý, log 350 → 550 B (starý řádek zachován), nová záloha `backup/2026-07-26_0648_memory.backup.md`.
- **Test 3a (chybová cesta):** kopie skriptu s `GOAL`, který vynucuje prózu → 3 pokusy, model postupně konvergoval (próza → JSON bez `changes` → JSON se špatným `op`), exit 1, na Telegram 145 znaků `Memory compact: CHYBA — po 3 pokusech: …`, `MEMORY.md` nedotčen.
- **Test 3b (zotavení po odmítnutí):** kopie s uměle odmítnutým prvním pokusem → follow-up přijat, druhý pokus prošel, exit 0.
- **`kimi27` uspěl na první dobrou** ve všech reálných bězích a i pod chybovým tlakem reagoval na konkrétní validační hlášky.
**Co zbývá:** Zkontrolovat noční běh 2026-07-27 (`log/compact_memory_auto_cron.log`) — odpověď modelu tam teď bude JSON, ale doručená zpráva z ní nevzniká. Zvážit zápis do `knowledge.md` (jak SDK přepíná model per běh, seznam presetů) a návrh do `decisions.md` (viz níže, čeká na formulaci autora). Interaktivní režim skillu zůstal beze změny a nebyl retestován.
**Pozor na `__pycache__`:** Lokální test skriptu přes `importlib` vyrobil `skills/compact-memory/scripts/__pycache__/`, který se `rsync -av` dostal na server (smazáno na obou stranách). Deploy skillů se skripty dělat s `--exclude='__pycache__'`.
**Jak vrátit zpět:** `git checkout 9c82894~1 -- skills/compact-memory/` + `rsync -av --exclude='__pycache__' skills/compact-memory/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/compact-memory/`. Testovací zásahy do `MEMORY.md` jsou vrácené (ověřeno `diff` proti kopii pořízené před testy, hash `249015fd…` shodný před i po). Testovací záznam v `log/memory-clean.log` a záloha `backup/2026-07-26_0648_*` zůstávají jako stopa po testu — log je append-only, nemažou se.
## 2026-07-27 — compact-memory: anglické hlášky + testy, a ruff/ty lint hook pro celý repo
**Cíl:** Tři věci na jedno zadání. (1) `compact_memory_auto.py` měl anglické docstringy, prompty i validátorové hlášky, ale sedm uživatelských hlášek zůstalo česky — doručený report byl jazykově rozpolcený. (2) Skript má bohatou čistou logiku (parser, validátor, přestavba souboru, report), ale jako jediný netriviální skill neměl `tests/`; offline testy parseru z 07-26 žily jen ve scratchpadu a zmizely. (3) V repu nebyla žádná Python lint konfigurace ani hook, přestože `skills/python/SKILL.md` definuje hotovo jako `ruff format` + `ruff check --fix`.
**Co jsem zkusil:** Plán `~/.claude/plans/vsechny-hlasky-do-anglictiny-curious-lantern.md`, schválený v plan mode.
- **Jazyk:** přeloženo všech 7 českých hlášek (`nebylo nic k smazání` → `nothing to remove`, `CHYBA —` → `ERROR —`, atd.). Součástí byla i změna `SKILL.md`: pravidlo `reason` — max 120 characters, **in Czech** → **in English** plus dva příklady v JSON schématu. Bez toho by report zůstal míchaný, protože `reason` od modelu se vkládá doslova do Telegram zprávy i do `log/memory-clean.log`.
- **Testy:** nový `skills/compact-memory/tests/test_compact_memory_auto.py` — 51 testů, vzor podle `skills/note/tests/test_note_compile.py` (`sys.path.insert` na `../scripts`, `tmp_path` fixtura). Pokrývá `_extract_json`, validaci všech polí, agregaci chyb, hledání/unikátnost bloku, překryv, 50% limit, `apply_change_set` (obsah, záloha, append-only log, počet řádků), `format_report`, `_log_line`, `_shorten`, `_quote`, `_telegram_config`, `_workspace`. Aby to šlo importovat bez `nanobot-ai`, přesunut `from nanobot import Nanobot` z module-level do `_run()` + `TYPE_CHECKING` import pro anotaci — stejný vzor, jaký už používá `note_compile.py`.
- **Lint:** nový `ruff.toml` (line-length 120, target py311, `select = ["E","F","I","UP","B","BLE","SIM","C4","RUF"]`), nový `ty.toml` (extra-paths na všechny `skills/*/scripts`, `unresolved-import = "ignore"`), nový hook `.claude/hooks/python-check.sh` registrovaný jako třetí položka v existující `PostToolUse` skupině `Edit|Write|MultiEdit`.
**Co fungovalo a proč:**
- **Hook je fail-fast a ověřený všemi cestami:** non-`.py` → exit 0; soubor pod `tmp/` → exit 0; neopravitelný ruff nález (`F821`) → exit 2 s `ruff findings`; čistý lint ale typová chyba → exit 2 s `ty findings`; čistý soubor → exit 0. Auto-fix ověřen (nepoužitý import ruff sám smazal). Latence: ruff ~10 ms, ty ~95 ms.
- **`unset VIRTUAL_ENV` v hooku je nutnost, ne opatrnost.** V tomhle prostředí je proměnná zděděná a ukazuje na neexistující `/home/lachtan/.pyenv/versions/3.11.5`; `ty` na tom padá s `Failed to discover local Python environment` ještě před kontrolou.
- **Explicitní `select` v `ruff.toml` je taky nutnost.** Ruff 0.16 má bez configu velmi širokou výchozí sadu (125 nálezů repo-wide včetně `DTZ*` a `EXE001`) a délku řádku 88, což odporuje `.claude/rules/python.md` (120). `BLE` je ve výběru schválně: skript má dvě promyšlená `# noqa: BLE001` a bez `BLE` by je `RUF100` označil za mrtvá a `--fix` je i s vysvětlujícím komentářem smazal.
- **Ruff sám opravil, co jsem nehledal:** `asyncio.TimeoutError` → `TimeoutError` (`UP041`) a sloučení tří zalomených `raise ChangeSetError(...)` na jeden řádek (dřív zalomené kvůli 88 znakům).
- **`ty` našel reálnou chybu:** `sorted(set(raw) - allowed)` — klíče jsou po `isinstance` zúžení typované `object`, což nesplňuje `SupportsRichComparison`. Opraveno na `sorted(str(key) for key in set(raw) - allowed)`.
- **Server byl před nasazením identický s repo HEAD** (`rsync` do `tmp/srv-check-cm/` + `diff -r`) — žádný drift od Dream procesoru. Po nasazení `md5sum` shodné na obou stranách, vlastník `nanobot:nanobot`, `py_compile` na serveru OK, crontab beze změny.
**Zádrhel — PEP 723 přepíná `ty` do script-mode, kde projektový `ty.toml` neplatí na pravidla.** `unresolved-import = "ignore"` fungovalo na běžné moduly, ale ne na `compact_memory_auto.py`. Izolováno probe souborem: identický soubor **s** `# /// script` hlavičkou hlásí `unresolved-import`, **bez** ní projde. `--project .` nepomůže; `--config "rules.unresolved-import='ignore'"` ano, ale flag v hooku by znamenal, že ruční `ty check` dělá něco jiného než hook. Řešení: inline `# ty: ignore[unresolved-import]` u obou `nanobot` importů + komentář v `ty.toml`, že sekce `[rules]` na PEP 723 skripty nedosáhne.
**Zádrhel — `rsync` dvou souborů naráz je položil do stejného cílového adresáře.** `rsync SKILL.md scripts/compact_memory_auto.py server:.../compact-memory/` udělal na serveru `compact-memory/compact_memory_auto.py` vedle `compact-memory/scripts/compact_memory_auto.py`. Stray kopie smazána, skript nasazen zvlášť do `scripts/`. Příště buď celý adresář, nebo jeden soubor na jeden `rsync`.
**Co zbývá:** Ostrý noční běh 2026-07-28 02:00 — první, který poběží s anglickými hláškami a anglickým `reason`; zkontrolovat `log/compact_memory_auto_cron.log` a nové řádky v `log/memory-clean.log` (historické české řádky zůstávají, log je append-only). Manuální ostrý běh jsem nedělal — mutuje reálný `MEMORY.md` a posílá Telegram. Repo-wide zbývá 92 ruff a 16 ty nálezů v ostatních skriptech; hook je nahlásí, až se těch souborů někdo dotkne (nejvíc `skills/llm-wiki/scripts/wiki_lint.py` = 9). Ostatní skripty s českými hláškami (`tasks-daemon.py`, `wiki_compile.py`, `note_compile.py`) zůstaly podle rozhodnutí česky. `.claude/rules/bash.md` řádek 45 tvrdí, že bash formátování hlídá `check-bash.sh` PostToolUse hook — ten v repu neexistuje ani není registrovaný; dokument je rozsynchronizovaný.
**Jak vrátit zpět:** `git checkout <tento commit>~1 -- skills/compact-memory/ .claude/settings.json` + `rm ruff.toml ty.toml .claude/hooks/python-check.sh` + `rsync -av --exclude='__pycache__' --exclude='tests' skills/compact-memory/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/compact-memory/`. Na serveru se nic nemazalo ani nemutovalo mimo dva přepsané soubory (`SKILL.md`, `scripts/compact_memory_auto.py`) — `MEMORY.md`, zálohy ani logy zásah nepotkal.
## 2026-07-27 — Kontrola presetu `kimi3` (Kimi K3 na Ollama Cloud) + oprava context window
**Cíl:** Ověřit, že nově vydaný Kimi K3 (`kimi-k3:cloud` na Ollamě) je v `~/.nanobot/config.json` nastavený správně a má správné parametry.
**Co jsem zkusil:**
- `ssh nanobot@nanobot.hell` → dump `config.json`. Pozor: top-level klíč je `model_presets` (snake_case), ne `modelPresets`, jak tvrdila knowledge — první pokus o výpis vrátil `null`.
- `curl http://nvidia.hell:11434/api/tags` (model v katalogu je) a `api/show` pro `kimi-k3:cloud` i pro srovnání `kimi-k2.6`, `kimi-k2.7-code`, `glm-5.1`, `glm-5.2`.
- Funkční test: `curl .../v1/chat/completions` s `kimi-k3:cloud`, pak totéž s `glm-5.2:cloud` a `kimi-k2.7-code:cloud` jako kontrolní skupina.
- `journalctl --user -u nanobot.service` — hledání stop po přepnutí na preset.
- Online ověření na žádost uživatele: [ollama.com/pricing](https://ollama.com/pricing), [ollama.com/library/kimi-k3](https://ollama.com/library/kimi-k3), oznámení Ollamy na X.
- `curl https://openrouter.ai/api/v1/models` — hledání alternativní cesty ke stejnému modelu.
**Co fungovalo a proč:**
- **Konfigurace presetu je věcně správná.** `kimi3` = `{model: kimi-k3:cloud, provider: ollama, maxTokens: 16384, temperature: 0.1, reasoningEffort: null}`. Název modelu přesně sedí s `api/tags`, `apiBase` má povinný `/v1` suffix, model má capabilities `tools`+`thinking`+`vision`. Journal potvrdil, že runtime přepnutí funguje: `Runtime model switched for next turn: glm-5.1:cloud -> kimi-k3:cloud`. `reasoningEffort: null` není opomenutí — `schema.py:141` říká, že `None` zachovává default providera.
- **Blokuje to Ollama, ne config.** Přímé volání endpointu vrací HTTP 402 `this model uses extra usage only (not included plan usage) and your extra usage balance is empty`. Kontrolní skupina (`glm-5.2:cloud`, `kimi-k2.7-code:cloud`) přitom odpovídá normálně → není to stav účtu ani API klíč, je to per-model politika. Online potvrzeno: K3 vyžaduje Pro/Max **a** nenulový dokoupený extra usage balance, kapacita se teprve rozšiřuje. Nanobotí hláška *„API key is out of quota or the account is in arrears"* je zavádějící překlad tohoto 402.
- **Oprava parametru:** `contextWindowTokens` bylo `967000` proti reálnému `1048576` (z `api/show`, klíč `kimi-k3.context_length`) — ~81k tokenů ležel ladem. Ostatní presety drží ~97 % reálného okna (`glm-5.1`: 196608/202752, `glm-5.2`: 976000/1000000, kimi K2.x: 262144 = přesně). Srovnáno na **1 020 000**. Záloha `~/.nanobot/backup/config.json.bak-20260727-202245`, `diff` proti ní ukazuje jediný změněný řádek, vlastník `nanobot:nanobot` a práva `600` beze změny. Restart nebyl potřeba — presety se načítají každý tah (`_refresh_provider_snapshot`).
- **Vedlejší nález:** knowledge tvrdila konvenci pojmenování presetů `<model>-<provider>` (`kimi-k2.6-openrouter`), reálně jsou na serveru krátké aliasy (`kimi`, `kimi27`, `kimi3`, `glm`, `glm52`, `sonnet`, `haiku`, `gemini-flash`). Opraveno v `knowledge.md` spolu s názvem klíče `model_presets`.
**Co zbývá:** Model je do dobití extra usage na ollama.com nepoužitelný. Alternativa bez dobíjení, pokud ji uživatel bude chtít: preset přes OpenRouter (`moonshotai/kimi-k3`, ctx 1 048 576, $3/M in + $15/M out) — provider `openrouter` je v configu už nakonfigurovaný. Nezakládal jsem ho, nebylo zadáno.
**Jak vrátit zpět:** `ssh nanobot@nanobot.hell 'cp ~/.nanobot/backup/config.json.bak-20260727-202245 ~/.nanobot/config.json'` — mimo tuhle jednu hodnotu jsem na serveru nic neměnil.

View File

@@ -80,6 +80,24 @@ Nanobot cron job **vždy** běží přes agenta (`agent.process_direct`) — nea
Plný rozbor: history 2026-05-27 "Spam Output was empty".
**Pozn. (2026-07-15):** Popis výše (`on_cron_job` hardcoded preamble + `evaluate_response` fail-open) platí pro starší nanobot. V `nanobot-ai` 0.2.2 je mechanismus jiný — viz "Cron `agent_turn` joby v 0.2.2 — `is_bound_cron_job`" níže. Závěr (LLM cron job pro non-reminder úlohy je nespolehlivý → řešit mimo `jobs.json`) platí dál, jen jinou cestou.
---
## Cron `agent_turn` joby v 0.2.2 — `is_bound_cron_job`, `session_key` nezávislý na doručení
V nainstalovaném `nanobot-ai` 0.2.2 nahradil starý "hardcoded reminder preamble + evaluate_response" mechanismus (viz sekce výše, teď zastaralá popiskem) nový: `dream` a `heartbeat` jsou speciální system joby v `on_cron_job` (`nanobot/cli/commands.py`); libovolný jiný `agent_turn` job z `jobs.json` jde přes `is_bound_cron_job()` (`nanobot/cron/session_turns.py`) → `run_bound_cron_job()` (`nanobot/cron/bound_runner.py`).
**`is_bound_cron_job(job)`** vrací `True`, když `payload.kind == "agent_turn"` a má `session_key`+`origin_channel`+`origin_chat_id`, a **žádné** z `deliver`/`channel`/`to`/`channel_meta` není nastavené. Bound joby se spustí přes `agent.submit_cron_turn(...)` **jako normální session turn****bez jakéhokoli delivery gatingu** (na rozdíl od `heartbeat`, který volá `evaluate_response(..., default_notify=False)` s fail-closed chováním). Cokoli agent v bound cron tahu odpoví, jde přímo do kanálu.
**`session_key` (kam se zapíše historie) je nezávislý na `origin_channel`/`origin_chat_id` (kam se doručí)** — `origin_delivery_context()` (`nanobot/cron/session_delivery.py`) čte výhradně `payload.origin_channel`/`origin_chat_id`, vůbec ne `session_key`. Lze tedy nastavit `session_key` na cokoli (jiná/izolovaná session, nebo — pokud jde mimo `jobs.json` — per-run unikátní hodnota) a doručení do daného kanálu/chatu zůstane funkční.
**Gotcha:** `jobs.json` neumí dynamický/per-run `session_key` (je to statický string v definici jobu) — takže "vždy čerstvá session" se **nedá** vyřešit uvnitř `jobs.json`, i když by teoreticky šlo nastavit fixní odlišnou session (izolovanou od živého chatu, ale pořád akumulující historii mezi běhy). Pro opravdu čistý kontext při každém běhu je potřeba jít mimo `jobs.json` úplně — Python API `Nanobot.run(session_key=…)` s novou hodnotou (např. timestamp) při každém spuštění, vzor `detach` (`f"detach:{path.stem}"`). `Nanobot.run(ephemeral=True)` (`nanobot.py:126-135`, `process_direct(ephemeral=True)` interně) existuje, ale neprochází channel-delivery pipeline — po ephemeral běhu by bylo nutné doručení řešit ručně (publish `OutboundMessage` nebo přímé Bot API volání).
**Praktický důsledek:** Cron job navázaný `session_key` na **stejnou session jako živý chat uživatele** (`f"{channel}:{chat_id}"`, typicky vznikne, když se job zakládá přímo z chatu) kontaminuje conversation historii mezi denním chatem a nočními běhy navzájem — omyl agenta v jednom běhu se táhne do dalších. Řešeno pro `compact-memory-auto-daily`: history 2026-07-15.
Zdroj: `nanobot/cron/session_turns.py:is_bound_cron_job`, `nanobot/cron/bound_runner.py:run_bound_cron_job`, `nanobot/cron/session_delivery.py:origin_delivery_context`, `nanobot/nanobot.py:126-165` (`Nanobot.run`).
---
## Workspace vzniká při prvním spuštění agenta
@@ -213,11 +231,12 @@ Zdroj: `nanobot/channels/telegram.py:258-326` (BotCommand registrace, regex rout
## Postup: přidání nového modelu (preset)
Modely se přidávají jako položky do `modelPresets` v `~/.nanobot/config.json` na serveru `nanobot.hell` (uživatel `nanobot`).
Modely se přidávají jako položky do `model_presets` v `~/.nanobot/config.json` na serveru `nanobot.hell` (uživatel `nanobot`). Pozor: top-level klíč je v souboru **snake_case** (`model_presets`), vnitřní klíče presetu camelCase (`maxTokens`, `contextWindowTokens`, `reasoningEffort`).
**Kroky:**
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`.
**Dostupnost v katalogu ≠ použitelnost** — u Ollama Cloud ověř i funkčním voláním: `curl http://nvidia.hell:11434/v1/chat/completions -d '{"model":"<id>","messages":[{"role":"user","content":"say OK"}],"max_tokens":20}'` (viz sekce o extra usage níže).
2. **Edituj config in-place** přes Python (zachová ostatní klíče včetně secrets):
```bash
@@ -238,10 +257,29 @@ Modely se přidávají jako položky do `modelPresets` v `~/.nanobot/config.json
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ů:** krátký alias podle modelu — `kimi`, `kimi27`, `kimi3`, `glm`, `glm52`, `sonnet`, `haiku`, `gemini-flash`. (Dřív tu stálo `<model>-<provider>` jako `kimi-k2.6-openrouter`; reálný stav na serveru je od nějaké doby krátká forma, ověřeno 2026-07-27.)
**Parametry presetu:** `maxTokens` 16384 a `temperature` 0.1 napříč všemi presety. `contextWindowTokens` se drží na **~97 % reálného okna modelu** (rezerva na výstup), reálné okno se čte z `curl http://nvidia.hell:11434/api/show -d '{"model":"<id>"}'` → `model_info["<family>.context_length"]`. `reasoningEffort: null` = zachovat default providera (`schema.py:141`), explicitní hodnota jen kde ji chceme vynutit (`glm52: high`).
**Ollama gotcha:** `providers.ollama.apiBase` musí končit `/v1` (`http://nvidia.hell:11434/v1`) — viz [[Ollama provider potřebuje `/v1` suffix v `apiBase`]].
## Ollama Cloud: některé modely jsou „extra usage only" (kimi-k3)
`kimi-k3:cloud` je v `api/tags` vidět a `/model kimi3` v nanobotu se přepne bez chyby, ale **každé volání skončí HTTP 402**:
```text
this model uses extra usage only (not included plan usage) and your extra usage
balance is empty, add extra usage or turn on auto reload at ollama.com/settings
```
Nanobot to uživateli přeloží na *„API key is out of quota or the account is in arrears"* — což svádí na chybu v configu; **není to chyba configu**. Ollama má dvě oddělené peněženky: *plan usage* (zahrnuto v Pro/Max) a *extra usage balance* (dokupovaný kredit). K3 je zařazený mimo plán a čerpá **jen** z extra usage — podle [model page](https://ollama.com/library/kimi-k3) vyžaduje Pro/Max **a** nenulový extra balance, kapacita se teprve rozšiřuje. Billing je GPU-time-based, per-token sazba pro extra usage není veřejně zveřejněná.
**Diagnostika:** ostatní cloud modely (`glm-5.2:cloud`, `kimi-k2.7-code:cloud`) přitom jedou normálně — když 402 sedí jen na jednom modelu, je to tahle politika, ne stav účtu.
**Alternativa bez dobíjení:** stejný model na OpenRouteru jako `moonshotai/kimi-k3` (ctx 1 048 576, $3/M in, $15/M out) — provider `openrouter` je v configu nakonfigurovaný.
Zdroj: [Ollama pricing](https://ollama.com/pricing), [ollama.com/library/kimi-k3](https://ollama.com/library/kimi-k3), ověřeno 2026-07-27 (history).
## Logování: gateway `-v`/`--verbose`, agent `--logs`
Nanobot defaultně **vypíná vlastní logy** (`logger.disable("nanobot")`), proto v běžném výstupu nic není. Zapínají se podle příkazu:
@@ -389,6 +427,70 @@ do meta-statusu.
**externím daemonem mimo agent loop** — viz "Detach skill" níže. Stejný pattern
už používá `/remind` (viz "Cron job s LLM agentem je nespolehlivý…" výše).
**AKTUALIZACE (ověřeno 2026-07-15 na `nanobot_ai-0.2.2` živém na serveru): tohle popisuje jen fallback pro "unbound" joby.** Skutečná `agent_turn` architektura je jiná — viz "Cron `agent_turn` job = normal session turn (bound cron, od `nanobot_ai` 0.2.2)" níže. `on_cron_job` v `cli/commands.py` má teď větev `is_bound_cron_job(job)` → `run_bound_cron_job(...)`; jen když job **není** bound (chybí `session_key`/`origin_channel`/`origin_chat_id`, nebo má vyplněné staré `deliver`/`channel`/`to`/`channel_meta`), spadne do staré větve výše s hardcoded preamblem — a ta dnes končí `CronJobSkippedError("unbound agent cron job must be recreated from a chat session")`, ne úspěšným během. Reálný job v `jobs.json` (`nanobot-version-check`) je bound.
---
## Cron `agent_turn` job = normal session turn (bound cron, od `nanobot_ai` 0.2.2)
**Kontext, který LLM dostane při odpálení cron jobu, je (skoro) identický s běžnou zprávou v té session, kde byl job vytvořen** — ne izolovaný/ephemerní jednorázový volání jako dřív (`process_direct`). Ověřeno čtením zdroje na serveru (`nanobot_ai-0.2.2.dist-info`), potvrzeno reálným `jobs.json` (job `nanobot-version-check`: `sessionKey: "websocket:52d0f338-…"`, `originChannel: "websocket"`, `originChatId: "…"`, `deliver: false`, `channel: null`).
**Tok (`nanobot/cron/bound_runner.py:run_bound_cron_job`):**
1. Job musí být **bound** (`nanobot/cron/session_turns.py:is_bound_cron_job`): `payload.kind == "agent_turn"` + `session_key`/`origin_channel`/`origin_chat_id` vyplněné + staré delivery pole (`deliver`/`channel`/`to`/`channel_meta`) prázdné. Bound joby vznikají tak, že se vytvoří z chatu (agent zná runtime `Channel`/`Chat ID` z kontextu a předá je do `cron` toolu).
2. Zpráva se vyrenderuje z šablony `templates/agent/cron_reminder.md`: *"The scheduled time has arrived. Execute this scheduled cron job now and report the result to the user in the same session."* + pravidla (mluv přímo, nenarrativizuj, žádné user ID, žádné "Done") + `Cron job: {{ message }}` (= `payload.message`).
3. Sestaví se `InboundMessage` s `channel`/`chat_id` = **origin** hodnoty z jobu, `session_key_override = payload.session_key`, a metadata `_cron_trigger` (job_id, job_name, run_id, prompt_ref) + `_cron_defer_until_session_idle: true`.
4. Ta zpráva jde přes `agent.submit_cron_turn()` → `CronTurnCoordinator.submit()` → **stejnou frontu `bus.consume_inbound()`/`_dispatch()`/`_process_message()` jako kterákoli chatová zpráva** (`agent/loop.py:run`/`_dispatch`). Žádná zkratka, žádný oddělený "cron mód" LLM volání.
**Co z toho plyne pro obsah promptu, který LLM vidí:**
- **Celý systémový prompt jako obvykle** — bootstrap soubory (`AGENTS.md`/`SOUL.md`/`USER.md`/`TOOLS.md`), `memory/MEMORY.md`, always-skilly (`ContextBuilder.build_messages`, stejná cesta jako běžný tah).
- **Celá historie té session** (`session_key` = session, ve které byl cron job vytvořen — typicky WebUI/Telegram konverzace), ne prázdná/ephemerní session. Cron zpráva se do historie i zapíše (`_persist_user_message_early` s `cron_history_overrides`), ale zobrazená verze v historii je zkrácená: `"Scheduled cron job triggered: <job_name>\n\n<payload.message>"`, ne celá vyrenderovaná šablona — a metadata na ní má `cron_job_id`/`cron_job_name`/`cron_run_id`/`cron_prompt_ref`.
- **Runtime context blok** (`Channel`/`Chat ID`/`Current Time`) ukazuje origin channel/chat_id jobu — model tedy vidí, do jakého kanálu odpovídá, stejně jako u normální zprávy.
- **Všechny toolly** jsou k dispozici stejně jako v normálním tahu (žádný omezený tool subset jako u Dream `build_dream_tools()`).
- **Odpověď se doručí automaticky** — finální text tahu (`_process_message` return) jde přes `bus.publish_outbound()` do origin channel/chat_id, přesně jako běžná odpověď. Žádný druhý fail-open `evaluate_response` gate (ten zůstal jen u `heartbeat` větve).
**Deferral:** pokud je cílová session zrovna aktivní (probíhá jiný tah), `CronTurnCoordinator.defer_if_active()` job zafrontuje a spustí ho, až se session uvolní (`publish_next_deferred` po dokončení běžícího tahu) — cron turn tedy nikdy neběží souběžně s live chatem ve stejné session, ale ani ho nezahodí.
**Praktický důsledek:** cron `agent_turn` job dnes **umí** to, co starý hardcoded preamble bránil (viz sekce výše) — protože jede jako normální tah, může provést multi-step úkol, zapsat soubor, a odpověď je jen finální zpráva tahu, ne meta-status obalený cizím promptem. `nanobot-version-check` job v produkčním `jobs.json` toho využívá (kontrola PyPI verze + podmíněné oznámení).
Zdroj: `nanobot/cron/bound_runner.py`, `nanobot/cron/session_turns.py`, `nanobot/agent/cron_turns.py` (`CronTurnCoordinator`), `nanobot/agent/loop.py:574-578,868-870,1035-1117` (`submit_cron_turn`, `_dispatch`), `nanobot/templates/agent/cron_reminder.md`, `nanobot_ai-0.2.2` na `nanobot.hell` (živý zdroj, ne upstream repo).
---
## Bound cron job sdílí session s chatem — důsledky pro růst kontextu, `/new` a (ne)možnost odpoutání
Navazuje na sekci výše. Bound cron job **nemá vlastní session** — `payload.sessionKey` je přesně ten samý klíč jako chat, odkud vznikl (`CronTool.set_context()`: `session_key = f"{channel}:{chat_id}"`). Každé odpálení jobu appendne celý svůj tah (trigger zpráva + tool volání + finální odpověď) do `session.messages` té konverzace — soutěží o prostor s reálným chatem, ne v izolaci.
**Co to omezuje a co ne:**
- **`maxMessages: 120`** (server `config.json`, `agents.defaults.maxMessages` / schema default 120) — do LLM promptu se replayuje jen posledních 120 zpráv (`session/manager.py::get_history`). Token náklad per-tah tedy neroste do nekonečna, stará historie se jen vysouvá z okna.
- **`idleCompactAfterMinutes: 0` na `nanobot.hell` — auto-compact (`AutoCompact`) je VYPNUTÝ.** Default v schématu je 15 min; tady je natvrdo 0, takže `check_expired()` (`agent/loop.py`, volané na 1s timeoutu hlavní smyčky) nikdy nic nekomprimuje na 8 zpráv + summary.
- I kdyby zapnutý byl: **periodický cron job v té session drží `session.updated_at` čerstvý** → session nikdy nevypadá jako idle → auto-compact by se pro ni stejně nikdy nespustil. Recurring cron bound na chat tedy fakticky blokuje auto-compact té session, i kdyby byl jinde v config zapnutý.
- Výsledek: `session.messages` na disku roste bez omezení (jen replay do LLM je capnutý na 120) a nic z toho neprochází konsolidací do `MEMORY.md`/Dream, dokud uživatel neudělá `/new`.
**`/new` (`cmd_new`, `command/builtin.py:205`) čistí přesně tenhle sdílený session_key** — `session.clear()` + na pozadí archivace starého obsahu přes `consolidator` (feed pro Dream). Protože cron job běží ve stejné session, `/new` v daném chatu **vyprázdní i kontext, který cron job uvidí** při příštím odpálení (stará historie se archivuje, ne ztratí — jen zmizí z "recent" replaye).
**Odpoutat cron job od session přes `cron` tool nejde — strukturálně.** `CronTool._add_job` (`agent/tools/cron.py`) bere `session_key`/`origin_channel`/`origin_chat_id` vždy z aktuálního request kontextu; bez nich vrátí chybu `"scheduled cron jobs must be created from a chat session"`. Parametr `deliver` v `execute()` existuje, ale nikam se nepředává do `add_job` — mrtvý pozůstatek staré (unbound) architektury. Ruční editace `jobs.json` na "unbound" tvar (bez `sessionKey`/`originChannel`/`originChatId`) taky nepomůže — `is_bound_cron_job()` vrátí `False` a `on_cron_job` rovnou shodí `CronJobSkippedError("unbound agent cron job must be recreated from a chat session")`; **stará větev s hardcoded preamblem (viz sekce "Cron preamble je hardcoded" výše) se pro `agent_turn` joby dnes nikdy neprovede — je to mrtvá cesta.**
**Pro skutečně nezávislou (session-decoupled) periodickou úlohu použij [[Detach skill]] níže** — vlastní `session_key` namespace (`detach:<slug>`), mimo cron/jobs.json úplně, doručení přímo přes Telegram Bot API bez účasti chatové session.
Zdroj: `nanobot/agent/tools/cron.py` (`CronTool._add_job`, `set_context`), `nanobot/agent/autocompact.py` (`AutoCompact`), `nanobot/config/schema.py:147-156` (`session_ttl_minutes`/`idleCompactAfterMinutes`, `max_messages`/`maxMessages`), `nanobot/session/manager.py` (`get_history` replay slicing), `nanobot/command/builtin.py:205` (`cmd_new`), živý `~/.nanobot/config.json` na `nanobot.hell` (`idleCompactAfterMinutes: 0`, `maxMessages: 120`) — ověřeno 2026-07-15.
### Upstream stav: žádný flag na vypnutí sdílení session zatím neexistuje (ověřeno 2026-07-15)
Sdílení session cronu s originálním chatem (viz výše) je **záměrný design**, ne bug: [PR #4299](https://github.com/HKUDS/nanobot/pull/4299) *"feat(cron): bind scheduled automations to sessions"* (mergnuto 2026-06-12) ho zavedlo cíleně, aby se vyřešilo doručování/injektování zpráv doprostřed živého chatu (`bound_runner.py` vznikl přesně touhle PR).
Na neomezený růst kontextu si ale stěžují i jinde a řeší se to teď (oba stavy **open, nemergnuto**):
- [Issue #4082](https://github.com/HKUDS/nanobot/issues/4082) — *"cron jobs reuse fixed cron:{job.id} session context across runs"*, navrhuje buď per-run isolaci, nebo *"an explicit config flag controlling cron context retention"*.
- [PR #4550](https://github.com/HKUDS/nanobot/pull/4550) — řeší to per-run izolovanou session (`session_key:run_id`, smazanou hned po tahu přes nový `agent.delete_session()`), **bez** configurovatelného flagu. Autor PR k automatizované review připomínce (ztráta cross-run kontinuity pro monitoring/diff joby) napsal: *"a future `shared_session` option could be added, but that's a design decision for maintainers"* — tedy jen nápad, ne odsouhlasený roadmap item, žádný maintainer zatím nezareagoval. `mergeable_state: dirty` (2026-07-06).
- Starší konkurenční [PR #2457](https://github.com/HKUDS/nanobot/pull/2457) (open od 2026-03-25) řeší podobný problém na starším kódu, pravděpodobně obsoletní vůči #4550.
**Souvisí:** [PR #4370](https://github.com/HKUDS/nanobot/pull/4370) *"Enable idle auto-compact by default"* (mergnuto 2026-06-16) změnilo schema default `idleCompactAfterMinutes` z `0` na `15` — právě kvůli krátkým chatům, co se nikdy nekompaktují. Náš server má v `config.json` **explicitní `0`** (přebíjí schema default bez ohledu na verzi) — nezávisí na tom, jestli byl nastaven před nebo po téhle změně.
**Akce:** sledovat #4082/#4550 (a případně #2457), jestli se mergne a jestli přibude `shared_session` flag — pak přehodnotit, jestli přenastavit `nanobot-version-check` (a budoucí podobné joby) na isolated mód. Zatím jediná cesta k plnému odpoutání = [[Detach skill]] níže.
---
## Detach skill — background úlohy přes externí daemon (mimo agent loop)
@@ -567,6 +669,22 @@ Přepsáno z SQLite row-store na **capture → compile pipeline** (vzor llm-wiki
- **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/`).
---
## Skill `/project` — pojmenované dlouhodobé pracovní kontexty (přepsáno 2026-07-22)
Substituce Claude.ai "Projects". Adresář na projekt, ne jeden soubor: `workspace/projects/<slug>/{prompt.md, memory.md, state.md, artifacts/}`. `prompt.md` = kontext/instrukce čtené při aktivaci; `memory.md` = append-only chronologická historie/rozhodnutí; `state.md` = **živý** dokument (přepisuje se na místě, syntéza "kde to teď je" — ne deník); `artifacts/` = generované soubory bez zvláštní evidence.
**Aktivní projekt = čistě konverzační paměť**, žádný stavový soubor na disku pro "co je aktivní" (viz [[decisions.md]]). Nový projekt vzniká jen po explicitním potvrzení uživatele — skill bez ptaní pracuje jen s existujícími projekty. Mazání/přejmenování projektu je mimo scope v1 (ruční zásah v adresáři).
**Nahradil netrackovaný server-side skill** (existoval na serveru mimo tento repo, žádná zmínka v history/knowledge/decisions před tímto datem): plochý soubor `projects/<slug>.md` s frontmatterem `status`/`priority`/`created`, CLI backend (`scripts/project.py`: add/list/show/status), `switch` ukládal aktivní projekt do `my` scratchpad nástroje (viz níže). Reálná data (`projects/radio-1.md`, projekt na stříhání audio streamu Radia 1, 2026-06-09) přemigrována do nového formátu jako `projects/radio1/`. Plná historie: history.md 2026-07-22.
## `my` nástroj — přece jen nějaký perzistentní scratchpad existuje
Zjištěno 2026-07-22 při objevu starého `/project` skillu výše: ten používal `my(action="set", key="project_context", value="<slug>")` k uložení aktivního projektu (a zjevně `action="get"` k přečtení). To je v napětí s dřívějším závěrem [[Bound cron job sdílí session s chatem]] výše, že nanobot nemá žádný session-scoped state kromě historie zpráv — `my` je zjevně nějaká forma key-value scratchpadu dostupná agentovi jako tool.
**Needs-verify:** starý skill sám dokumentoval jen „lives only in `my` scratchpad and is lost on restart" — neříká, jestli je klíč per-`session_key` nebo globální napříč celým agentem. Nebylo ověřeno proti zdrojáku (`nanobot/agent/tools/*.py`), jen pozorováno z použití v `SKILL.md` starého skillu (kopie stažená do `tmp/server-project-check/`, negitované). Pokud je globální, nemusí řešit "aktivní projekt per konverzace" o nic líp než čistě konverzační paměť — proto zůstal nový `/project` skill navržen bez `my`. Stojí za ověření zdrojáku, než se `my` použije jinde jako spolehlivý state store.
## 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).