Files
nanobot-runtime/develop/knowledge.md
2026-06-24 08:11:12 +02:00

745 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Knowledge
Ověřená fakta o vnitřním fungování nanobota. Stručně, s případným odkazem na zdroj pokud je to oprvdu podstatné.
---
## Kdy je a není potřeba restart nanobot.service
**Restart NENÍ potřeba:**
| Soubor | Proč |
|---|---|
| `~/.nanobot/workspace/cron/jobs.json` | Cron service volá `_load_store()` při každém ticku — soubor se načte znovu automaticky |
| `~/.nanobot/workspace/reminder.yaml` | Čte ho `remind_check.py` jako subprocess; každé spuštění čte čerstvě |
| Skripty v `workspace/skills/` | Exec tool je spouští jako subprocess pokaždé znovu |
| `~/.nanobot/config.json`**providers a modelPresets** | `_refresh_provider_snapshot()` volá `load_config()` před každým agentem tahem; `/model` přepínač funguje okamžitě |
**Restart JE potřeba:**
| Soubor / změna | Proč |
|---|---|
| `~/.nanobot/config.json` — channels, tools, MCP servery, workspace | Tyto sekce se předávají do `AgentLoop.from_config()` jednou při startu |
| `~/.config/systemd/user/nanobot.service` | Po změně: `daemon-reload` + restart |
**Zdroj:** `nanobot/agent/loop.py:_refresh_provider_snapshot()`, `nanobot/cron/service.py:_load_store()`, `nanobot/providers/factory.py:load_provider_snapshot()`
---
## Restart nanobot.service jako root
`systemctl --user restart nanobot.service` jako root **selže** — user bus není dostupný bez správných env proměnných. Správný postup:
```bash
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
'
```
**Pozor:** `kill -HUP <pid>` na gateway proces nanobot **nezrestartuje** — proces se ukončí a systemd ho nenaskočí zpět (není to watchdog). Místo HUP vždy používej `systemctl --user restart`.
---
## Cron joby nanobota: jobs.json
Naplánované joby jsou v `/home/nanobot/.nanobot/workspace/cron/jobs.json`. Struktura: pole `jobs`, každý má `id`, `schedule` (kind=`cron`/`every`/`at` s `expr`/`every_ms`/`at_ms`, volitelně `tz`), `payload` (kind=`agent_turn`, `message`, `channel`, `to`, `channelMeta`, `deliver`), volitelně `deleteAfterRun` (true pro `at` joby = jednorázové).
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ř.:
```bash
ssh root@nanobot.hell "python3 -c \"
import json; from pathlib import Path
p = Path('/home/nanobot/.nanobot/workspace/cron/jobs.json')
data = json.loads(p.read_text())
# ... uprav data ...
p.write_text(json.dumps(data, ensure_ascii=False, indent=2))
\""
```
---
## Cron job s LLM agentem je nespolehlivý pro "pošli jen když něco je"
Nanobot cron job **vždy** běží přes agenta (`agent.process_direct`) — neagentní typ jobu neexistuje. Dva problémy v cestě prázdného výstupu:
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ý.
**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).
Plný rozbor: history 2026-05-27 "Spam Output was empty".
---
## Workspace vzniká při prvním spuštění agenta
`~/.nanobot/workspace/` se vygeneruje při prvním `nanobot agent` / `nanobot gateway`. Obsahuje `AGENTS.md`, `USER.md`, `SOUL.md`, `HEARTBEAT.md`, `TOOLS.md`, `memory/`, git store.
## Co se auto-loaduje do system promptu (verze 0.2.0)
**Každý tah** ContextBuilder skládá system prompt z těchto zdrojů (žádná cache, fresh `read_text()`):
- **Bootstrap files** v rootu `~/.nanobot/workspace/`: `AGENTS.md`, `SOUL.md`, `USER.md`, `TOOLS.md`. Po editaci **není potřeba restart service** — změna platí od příští zprávy.
- Zdroj: `nanobot/agent/context.py:25` (`BOOTSTRAP_FILES`), `context.py:156` (`_load_bootstrap_files`).
- **`memory/MEMORY.md`** — hardcoded cesta v `MemoryStore`. **Žádný jiný soubor v `memory/` se NEčte** (ani `.bak`, ani user-vytvořené `.md`). `history.jsonl` konzumuje výhradně Dream procesor.
- Zdroj: `nanobot/agent/memory.py:55` (`memory_file = memory_dir / "MEMORY.md"`), `memory.py:205,229`.
- **Skilly s `metadata.always: true`** ve frontmatteru `workspace/skills/<name>/SKILL.md` — přes `SkillsLoader.get_always_skills()`. Ostatní skilly se nahrávají on-demand, ne do system promptu.
- Zdroj: `nanobot/agent/skills.py:203`.
**HEARTBEAT.md není v system promptu každého tahu** — má vlastní mechanismus přes `heartbeat/service.py`, čte se jen na heartbeat tick (default 30 min).
**Důsledek:** Když agent v chatu vytvoří soubor v `memory/` mimo `MEMORY.md` (např. `memory/film_policy.md`), tváří se jako že si pravidlo „uložil", ale **agent ho v dalším tahu neuvidí**. Místo toho ho musí jít do bootstrap souboru — viz následující sekce.
## K čemu slouží jednotlivé workspace soubory
| Soubor | Doména | Co tam patří | Co tam nepatří |
|---|---|---|---|
| `SOUL.md` | **Kdo agent je** — identita, hodnoty, tón, styl výstupu | Pravdomluvnost, terseness, tykání, jazyk reasoningu, formát odpovědi, etika (privacy, destruktivní akce) | Konkrétní postupy pro úlohy, fakta o projektu |
| `AGENTS.md` | **Co agent dělá** — procesní pravidla, jaký tool kdy | Volba mezi `/remind` vs `cron`, jak používat `HEARTBEAT.md`, varování typu „nepiš reminder do MEMORY.md" | Identita, hodnoty, fakta o uživateli |
| `USER.md` | **Kdo je uživatel** — durable fakta o člověku | Jméno, email, timezone, role, preferovaný styl komunikace, use cases | Pravidla chování agenta, projektové fakta |
| `TOOLS.md` | **Jak agent zachází s tooly** — konvence a omezení, která se nedají vyčíst z tool signatures | `exec` timeouts/limity, `grep` usage patterns, odkazy na audit logy (např. `log/reminder.log`) | Globální chování (to je SOUL), procesní pravidla (to je AGENTS) |
| `memory/MEMORY.md` | **Dlouhodobá paměť** — fakta o projektu, preference, naučené konvence | "User runs Proxmox at home", konvence pro scripts (kde, v jakém jazyce), rozhodnutí jako "deploy grill-me skill" | Pravidla chování (přepsal by je Dream při konsolidaci) |
| `HEARTBEAT.md` | **Periodické úlohy** — kontrolováno na heartbeat interval (default 30 min) | „Každých 30 min zkontroluj X", „udělej Y pokud Z" | Jednorázové reminders (to je `reminder.yaml` přes `/remind`) |
**Test umístění** (rozhoduj podle otázky, ne podle obsahu pravidla): „Mění to **kdo jsem** (SOUL) / **co dělám** (AGENTS) / **kdo je uživatel** (USER) / **jak používám tool** (TOOLS) / **co vím o projektu** (MEMORY) / **co dělám pravidelně** (HEARTBEAT)?"
Zdroj: upstream `nanobot/templates/{AGENTS,SOUL,USER}.md` (header docstrings), `nanobot/agent/context.py`, `nanobot/agent/memory.py`, `nanobot/heartbeat/service.py`.
## Ollama provider potřebuje `/v1` suffix v `apiBase`
Nanobot volá **OpenAI-kompatibilní `/v1/chat/completions`**, ne Ollama-native `/api/chat`. V configu musí být `apiBase: http://host:11434/v1` — bez `/v1` vrací Ollama 404.
`docs/configuration.md` to v příkladu (`http://localhost:11434`) **neuvádí** — je to zavádějící.
## modelPresets = jeden agent, víc modelů
Nanobot **nepodporuje víc pojmenovaných agentů**. Místo toho má `modelPresets` — pojmenované dvojice `(provider, model)`, mezi kterými se přepíná za běhu příkazem `/model <preset>` v chatu (Telegram i WebUI). Default je `agents.defaults.modelPreset`.
## Gateway s `websocket.host: 0.0.0.0` bez tokenu odmítne start
Bezpečnostní pojistka — pokud má WebSocket channel `host: 0.0.0.0` (bind všech rozhraní), vyžaduje vyplněný `token`. Jinak gateway selže při startu.
## Porty gateway
| Port | Co tam je |
|---|---|
| **8765** | WebUI HTML (SPA) + WebSocket auth endpoint na stejném portu |
| **18790** | Gateway health endpoint (`/health``{"status":"ok"}`) |
## CLI chat mód: `nanobot agent`
Interaktivní konverzace s agentem přímo v terminálu se spouští příkazem `nanobot agent`. Je to stejný agent jako přes Telegram/WebUI a sahá do stejného `~/.nanobot/workspace/` (sdílí paměť, bootstrap soubory i git store). Fungují v něm i slash-příkazy (`/model <preset>`, `/restart`, `/history`, `/status`, `/goal`, …).
Přehled CLI módů: `nanobot onboard` (setup wizard), `nanobot agent` (chat v terminálu), `nanobot gateway` (WebSocket gateway pro WebUI/Telegram).
Zdroj: upstream HKUDS/nanobot Quick Start („3. Chat: `nanobot agent`").
## `/model` bez argumentu vypíše dostupné presety
V chatu (CLI `nanobot agent` / Telegram / WebUI) napsání samotného `/model` (bez argumentu) vypíše status: aktuální model, aktuální preset a seznam dostupných presetů. `/model <preset>` přepne. Stejný seznam se ukáže i při pokusu přepnout na neexistující preset.
Seznam ukazuje **nakonfigurované `modelPresets`** z `~/.nanobot/config.json`, ne katalog modelů, co provider reálně nabízí (na to viz Ollama `…/api/tags`, OpenRouter `…/api/v1/models`). OpenAI-kompatibilní endpoint `/v1/models` vrací taktéž jen presety.
Zdroj: `nanobot/command/builtin.py` (`cmd_model`, `_model_command_status`).
## Telegram bot commands — `/new` resetuje session, `/restart` ne
V Telegramu jsou slash-příkazy zaregistrované jako **`BotCommand`** (objeví se v menu po stisku `/` v inputu). Nejsou to volné texty pro agenta — regex router (`_forward_command`) je posílá rovnou do AgentLoop, agent je v promptu nevidí.
| Příkaz | Co dělá |
|---|---|
| **`/new`** | **Reset session.** Zruší aktivní task, vyprázdní zprávy v sessionu, snapshot pošle do Consolidatoru na archivaci na pozadí. Tohle je „clear context" před novou diskuzí. |
| `/restart` | Restartuje **bota (proces)**, ne session — po restartu konverzace pokračuje. Slouží k načtení nové konfigurace, ne k čistění kontextu. |
| `/stop` | Zruší aktuálně běžící task, kontext nechá. |
| `/history` | Vypíše posledních N zpráv (read-only). |
| `/status`, `/goal`, `/pairing`, `/model`, `/dream`, `/dream_log`, `/dream_restore`, `/help` | Ostatní registrované commands. |
Pozn. k aliasům: Telegram nepovoluje pomlčku v command jménu, takže `/dream_log` a `/dream_restore` jsou aliasy — handler je interně přemapuje na kanonické `/dream-log` a `/dream-restore` (`_normalize_telegram_command`).
Zdroj: `nanobot/channels/telegram.py:258-326` (BotCommand registrace, regex router, alias normalizace), `nanobot/command/builtin.py:199` (`cmd_new``session.clear()` + background `consolidator.archive(snapshot)`).
## /remind skill — architektura a gotchas
**Úložiště = SQLite `~/.nanobot/workspace/db/reminders.sqlite`** (dřív `reminder.yaml`; migrace 2026-06-10). Schema: `reminders` (id, text, enabled, timezone, created_at, updated_at, deleted_at) + tři schedule tabulky `schedule_at`/`schedule_cron`/`schedule_random` (FK na reminder, cascade) + `reminder_fires` (audit jednotlivých odpalů: schedule_type, fire_time, delivered_at, status, error_message). Doručuje **systémový crontab uživatele nanobot** (každou minutu), který spouští `skills/remind/scripts/remind_send.py` přes `uv run` — čte DB, porovnává cron / `at` / random s Prague časem, při shodě posílá **přímo přes Telegram Bot API** (token z `config.json`, chat_id z `channels.telegram.allowFrom[0]` s fallback konstantou). Žádný agent, žádný LLM. Deduplikace přes tabulku `reminder_fires` (klíč reminder_id + schedule_id + schedule_type + fire_time, status='delivered'). `log/reminder.log` je teď provozní/debug log **všech operací** (ADD/EDIT/…/DELIVER, UTC) — ne zdroj pravdy pro doručení (viz `delivered` níže); `log/reminder_cron.log` zachytává stdout/stderr crontabu (zdravý běh = prázdný).
**Proč mimo agenta:** dřív to byl nanobot cron job `remind-check` přes agenta — spamoval "Output was empty" kvůli fail-open evaluatoru (viz výše "Cron job s LLM agentem"). Crontab to obchází deterministicky.
**Nesahej na to přes cron tool:** nikdy nevytvářet `remind-check` job v `cron/jobs.json`. Doručování řeší crontab mimo nanobot.
**Nevytvářet ani agentní delivery skill** (např. `deliver-reminder-notifications`, který by exec-em volal nějaký `remind_check.py`). Žádný takový skript v `remind/scripts/` není — je tam jen `remind_send.py` volaný cronem. Migrace na deterministické doručování ho udělala zbytečným. Pokud takový skill ve `workspace/skills/` najdeš, je to mrtvý zbytek a smaž ho.
**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.
**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).
**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).
**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`.
**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ě.)
### Vyřešené chyby
**schedule_type collision (fix 2026-06-10):** *Problém*`reminder_fires.schedule_type` se pro cron/random zapisoval špatně (`'at'`) a dedup pro ně nefungoval (maskovala jen 60s tolerance). *Příčina*`schedule_{at,cron,random}` mají každá vlastní AUTOINCREMENT id (překryv 1..N); `remind_send.main()` odvozoval typ přes `SELECT … UNION ALL …` a bral první shodu, vždy `'at'`. *Fix* — každá `_due_*` vrací svůj `schedule_type`, inference smazána. Reálně potvrzeno na prod (reminder 7 „Panama" random odpal zapsán jako `'at'`). Test `test_schedule_type_correct_despite_id_collision`. Plný záznam: history 2026-06-10.
**Duplicitní text (vyřešeno migrací na SQLite):** Starý YAML systém duplicity neuměl ani smazat (`remove --keyword``ambiguous` bez rozlišení), ani spolehlivě odpálit (dedup `sha1(text)` se přepisoval). SQLite to řeší: dedup per `reminder_id` (nezávislý odpal) + `--id` selektor (jednoznačné mazání/edit). Starý rozbor: history 2026-06-02.
## 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`).
**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`.
2. **Edituj config in-place** přes Python (zachová ostatní klíče včetně secrets):
```bash
ssh nanobot@nanobot.hell 'python3 -c "
import json, pathlib
p = pathlib.Path.home() / \".nanobot/config.json\"
c = json.loads(p.read_text())
c[\"modelPresets\"][\"<preset-name>\"] = {\"provider\": \"<ollama|openrouter>\", \"model\": \"<model-id>\"}
p.write_text(json.dumps(c, indent=2))
"'
```
3. **Restartuj službu**, aby gateway preset načetla:
```bash
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>`.
**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.
**Ollama gotcha:** `providers.ollama.apiBase` musí končit `/v1` (`http://nvidia.hell:11434/v1`) — viz [[Ollama provider potřebuje `/v1` suffix v `apiBase`]].
## 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:
- **`nanobot gateway -v` / `--verbose`** → INFO+DEBUG do stderr (u nás přes systemd do journalu). Pokrývá **i WebUI** — WebUI je jen `websocket` channel uvnitř gateway procesu (port 8765), není to samostatná služba, takže žádný separátní přepínač pro WebUI neexistuje.
- **`nanobot agent --logs` / `--no-logs`** → runtime log přímo v interaktivním CLI chatu. Jiný flag než gateway, nejsou zaměnitelné.
Žádná env proměnná ani config klíč pro log level mimo tyhle flagy neexistuje.
**Nasazení u nás:** `-v` přidáno do `ExecStart` v `~/.config/systemd/user/nanobot.service` na `nanobot.hell`. Logy živě: `ssh nanobot@nanobot.hell 'journalctl --user -u nanobot.service -f --no-pager'`.
**Co `-v` ukáže v jednom tahu** (ověřeno na WebUI zprávě):
- `Processing message from <channel>:<id>: <text>` — příchozí zpráva
- stavy agentního tahu: `RESTORE → COMPACT → COMMAND → BUILD → RUN → SAVE → RESPOND` (každý s časem)
- `Tool call: <nástroj>({...args...})` — **volání toolu i s argumenty** (INFO)
- `LLM usage: prompt=… completion=… cached=…` — spotřeba tokenů každé iterace agentní smyčky
- `Response to <channel>:<id>: <text>` — finální odpověď
**Co se NEloguje:** tělo tool výsledku (stdout), plné LLM zprávy ani thinking. Thinking jde samostatným kanálem do klienta (WebUI), ne do journalu. `-v` je serverová záležitost — ve WebUI se nic nezmění.
**Pozor:** `-v` zapíná INFO+DEBUG globálně, takže v journalu jsou i heartbeat/cron/dream tahy.
Startup taky vypíše užitečné: `Registered N tools: [...]` (výčet dostupných toolů) a `Runtime model switched … <model>` (aktivní preset).
## Srovnání modelů pro nanobot (cloud inference)
Hodnoceno pro mix: agentní úlohy (tool use, Dream, skilly) + rychlost + Python. Platí pro cloud Ollama i OpenRouter — hardwarové podmínky jsou srovnatelné. **Provider-agnostic pohled** (předpokládá dostupnost rychlé inference).
> **Za podmínky Ollama Cloud (žádný rychlý provider) to upřesňuje [`models.md`](models.md)** — tam rozhoduje latence, takže pro interaktivní vrstvu vede **GLM-5.1**, ne Kimi. Tahle tabulka a `models.md` se nerozcházejí v datech, jen v východisku: provider-agnostic vs. fixní Ollama Cloud.
| Pořadí | Model | Proč |
|--------|-------|------|
| 1 | **Kimi K2** (`kimi-k2.6-*`) | Jediný explicitně trénovaný na agentní úlohy a tool use; MoE ~32B aktivních params = rychlý |
| 2 | **Qwen 3.6+** (`qwen-3.6-plus-openrouter`) | Pravděpodobně Qwen3 235B-A22B (~22B aktivních = nejrychlejší v seznamu); top coding, silné instruction following |
| 3 | **DeepSeek V3.2** (`deepseek-v3.2-ollama`) | Nejlepší Python, nejsilnější instruction following; ~37B aktivních; ideální pro Dream |
| 4 | **Qwen 3.5** (`qwen3.5-ollama`) | Solidní záloha, dobrý coding, rychlý |
| 5 | **GLM-5.1** (`glm-5.1-ollama`) | Dobrý model, ale za Kimi/Qwen/DeepSeek na všech osách |
| 67 | **MiniMax M2** (obě varianty) | Nejméně prověřený pro agentic workload; rezerva pro speciální případy |
**Prakticky:** primary model → `kimi-k2.6`; Dream (pokud chceš jiný preset) → `deepseek-v3.2` nebo `qwen-3.6-plus`.
## Jak funguje nanobot skill systém
Skill = složka `~/.nanobot/workspace/skills/<name>/` se souborem `SKILL.md` (YAML frontmatter s `name` + `description`, tělo markdown instrukce). Bootstrap soubory se čtou při každém tahu bez restartu. Žádný `install` příkaz neexistuje — skill se vytvoří ručně (nebo ho Dream vytvoří sám).
**Clawhub.ai / OpenClaw** je jiný ekosystém, nemá s nanobotem nic společného. Skilly odtud je třeba manuálně adaptovat.
**Claude Code skilly jsou přímo přenositelné.** Anthropic Skills format (`SKILL.md` s YAML frontmatter `name`+`description` + markdown tělo) je identický s nanobot skill formátem. Stačí zkopírovat složku `skills/<name>/` ze zdroje (např. plugin `.claude-plugin/skills/<name>/`) do `~/.nanobot/workspace/skills/<name>/` — žádná konverze. **Manifest `.claude-plugin/plugin.json` se neinstaluje**, je Claude-Code-specific. Pozor jen na (a) reference na Claude-Code tooly v těle skillu (`TodoWrite`, `ExitPlanMode`, `AskUserQuestion` apod. v nanobotovi neexistují), (b) prompt-injection v markdown těle — nanobot čte skill jako součást system contextu. Ověřeno: nasazen `grill-me` z [mattpocock pluginu](https://github.com/lachtan/nicecode/tree/master/plugins/mattpocock) (history 2026-05-28 "Pilot mattpocock skillu grill-me").
Zdroj: `nanobot/agent/skills/`, `ContextBuilder._load_bootstrap_files()`
## 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:
- `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.
- **Always skilly** (`metadata.nanobot.always: true`): `description` se **ignoruje úplně**, do promptu se eager vkládá **celé tělo** (bez frontmatteru). Druhý vysvětlující odstavec v `description` je u nich čistý šum.
**Co tedy patří do `description`:** jen *kdy/proč* skill spustit — krátká věta o účelu + trigger fráze + případné odlišení od příbuzného skillu. **Nepatří** tam *jak* skill funguje (to do těla, čte se on-demand) ani detailní postup. Triggery nemusí být dvojjazyčné — model rozpozná záměr napříč jazyky, takže explicitní CZ varianty nic nepřidají, jen prodlužují řádek (ověřeno na `/plan`, 2026-05-31).
Zdroj: `nanobot/agent/skills.py:111-159` (`build_skills_summary`, `_get_skill_description`), `skills.py:94-109` (`load_skills_for_context`, always skilly), `nanobot/agent/context.py:87-95`.
## Dream procesor — automatické self-improvement
Nanobot má vestavěný Dream procesor (`agent/memory.py:Dream`) který běží každé 2 hodiny. Jde o **dvou-fázový LLM pipeline** nad `history.jsonl`:
- **Fáze 1:** Plain LLM call analyzuje historii, hledá fakta (`[MEMORY]`/`[USER]`/`[SOUL]`), kandidáty na smazání (`[FILE-REMOVE]`), opakující se workflow (`[SKILL]`)
- **Fáze 2:** AgentRunner s `read_file`/`edit_file`/`write_file` tools provede chirurgické editace; umí sám vytvářet nové skilly (`skills/<name>/SKILL.md`)
Dream řeší: deuplikaci, detekci stale obsahu (git blame age na řádcích MEMORY.md), automatické git commity po změnách. Cursor v `.dream_cursor` zabraňuje přepracování.
**Důsledek:** Self-improving-agent skilly z jiných ekosystémů jsou z velké části redundantní — Dream pokrývá jejich core funkcionalitu nativně. Přidaná hodnota by byl jen okamžitý strukturovaný error log (ERR-YYYYMMDD-XXX formát) — Dream čeká 2h.
Zdroj: `nanobot/agent/memory.py:Dream`, prompt templates `agent/dream_phase1.md`, `agent/dream_phase2.md`
---
## Non-interactive nanobot CLI: streamuje chaoticky, Python API vrací čistý string
`nanobot agent --message "..." --session "..."` projede agent loop, výstup
ale **streamuje rozkouskovaně přes stdout** (`` prefixované delty
reasoning/progress, finální `response.content` až na úplném konci). I při
`--no-markdown` a pipe (`| cat`) jde streaming dál. Postprocesovat by bylo
křehké.
**Pro programatické použití** (daemon, skript) jdi přes Python API:
```python
import asyncio
from nanobot import Nanobot
bot = Nanobot.from_config()
result = await bot.run("prompt", session_key="my:session")
# result.content je čistý string, žádné streamovací nečistoty
```
Interpreter s `import nanobot`: `/home/nanobot/.local/share/uv/tools/nanobot-ai/bin/python`.
Loguru jde na stderr (lze odchytit nebo přesměrovat). `Nanobot.run` interně volá
`AgentLoop.process_direct` **bez cron preamble** — to je jen v `on_cron_job` callbacku.
Zdroj: `nanobot/cli/commands.py:1204-1231` (CLI), `nanobot/nanobot.py:71-102` (`Nanobot.run`).
---
## Agent vidí `Channel` a `Chat ID` v runtime contextu zprávy
ContextBuilder každý tah příchozí zprávy obaluje runtime context blokem,
ve kterém je `Channel: <name>` a `Chat ID: <id>` (kromě `Current Time` a
volitelně `Sender ID`). Skill nebo prompt si je tedy **může přečíst** —
nemusí mít vlastní tool ani contextvars přístup.
```
Channel: telegram
Chat ID: 8826147089
```
V CLI / SDK session bez channel kontextu se blok nezobrazí (`Chat ID`
chybí). Skill na to musí umět reagovat (např. `detach` v takovém
případě nabídne synchronní vykonání).
Zdroj: `nanobot/agent/context.py:123-139` (`ContextBuilder._build_runtime_context`).
---
## Cron preamble je hardcoded — pro non-reminder background úlohy obejít
`nanobot/cli/commands.py:891-897` (`on_cron_job`) obaluje payload natvrdo:
```
The scheduled time has arrived. Deliver this reminder to the user now,
as a brief and natural message in their language. Speak directly to them —
do not narrate progress, summarize, include user IDs, or add status reports
like 'Done' or 'Reminded'.
Reminder: <payload.message>
```
Pro reminders je to správné chování. Pro background **úlohy** (deep research,
ingest, multi-step research) je to v přímém rozporu — agent má provést úkol,
zapsat výsledek do souboru, vrátit informativní větu. Preamble ho stáhne
do meta-statusu.
**Cesta okolo:** zahodit cron tool i `at` jednorázové joby, orchestraci řešit
**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).
---
## Detach skill — background úlohy přes externí daemon (mimo agent loop)
Architektura podobná `/remind` — orchestrace mimo agent loop, žádný cron preamble.
**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`.
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"]`).
**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:**
- `~/.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/create-task.py` — capture skript (frontmatter + atomický tmp→inbox)
- `~/.nanobot/workspace/skills/detach/scripts/{list-tasks,read-task}.py` — list / read subactions
- `~/.nanobot/workspace/skills/detach/tests/` — pytest čisté logiky (lokálně v repu, ne na serveru)
- `~/.nanobot/workspace/skills/detach/scripts/tasks-daemon.py` — daemon
- `~/.nanobot/workspace/skills/detach/systemd/tasks-daemon.{path,service}` — user systemd unity (symlinkované do `~/.config/systemd/user/`)
- `~/.nanobot/workspace/tasks/{tmp,inbox,running,done,failed}/` — fronty
- `~/.nanobot/workspace/log/tasks-daemon.{log,stdout.log,stderr.log}` — append-only logy
**Souběh:** systemd serializuje (`Type=oneshot` se nespustí podruhé, dokud první běh trvá; level-triggered `.path` ho restartne po doběhu pokud inbox stále není prázdný). Žádný flock není potřeba.
**Notifikační target — Telegram s fallback chat_id (single-user setup):** Skill v frontmatteru zapíše `channel` + `chat_id` z runtime contextu (`Channel: telegram` → numeric ID, `Channel: websocket` → session UUID, atd.). Daemon `resolve_telegram_chat_id(fm)`:
- pokud `channel == "telegram"` → použij `chat_id` z frontmatteru (multi-user ready)
- jinak → čti `channels.telegram.allowFrom[0]` z `~/.nanobot/config.json`
Tím Telegram vždy doručí, i když úkol přišel z WebUI / CLI. Daemon log: `NOTIFY chat=<id> source=<frontmatter|fallback>`. Bez tohoto fallbacku selhával Telegram Bot API s HTTP 400 pro non-telegram channel (history 2026-05-28 18:37).
**Subactions `list` a `read`:** detach skill umí i číst zpět hotové úkoly. „výsledky?" → markdown tabulka tasks/{running,done,failed}/. „výsledek <slug-nebo-pattern>" → `read_file` přes match v done/+failed/, předlož `# Result` sekci. Identifier match: slug substring (`*foo*`), timestamp fragment (`*T175451*`), nebo prázdný = nejnovější.
**Zdroj:** [skills/detach/](skills/detach/) v tracking repu, history 2026-05-28 „Skill detach + daemon" + iterace #2 + iterace #3.
**uv-native invokace (iterace #3):** Shebang přepnut na `#!/usr/bin/env -S uv run --script` s PEP 723 inline metadata (`requires-python = ">=3.11"`, `dependencies = ["nanobot-ai"]`). `uv run --script` samo vytvoří/cachuje izolované venv — skript přežije `uv tool uninstall/install` i přesun na jiný stroj. První spuštění po PEP 723 změně trvá ~5-10s (budování venv), další jsou instantní (cache v `~/.cache/uv/`). Systemd user unit musí mít `Environment=PATH=%h/.local/bin:/usr/bin:/bin`, jinak `uv` v PATH chybí.
---
## Detach notifikace do origin kanálu (WebUI/CLI) — záměrně nepodporováno
Daemon notifikuje **jen Telegram** (přes Bot API, deterministicky). Když task přišel z WebUI nebo CLI, do toho kanálu se notifikace nepošle — uživatel si výsledek vyzvedne přes `výsledek <slug>` (detach subaction `read`).
**Architektonický důvod:** WebSocket spojení vlastní gateway proces; daemon je samostatný systemd oneshot. Nanobot nemá HTTP endpoint pro vstřikování zpráv do WS sessions (`nanobot/channels/websocket.py:673-782` — všechny `/api/sessions/...` jsou read-only). Sdílí jen filesystem, žádné IPC.
**Zvážené a zamítnuté možnosti:**
- **Samostatný `Nanobot.run()` jen kvůli notifikaci** — LLM jako IPC proxy. Pomalé (1030 s), drahé, nedeterministické (model může prompt překroutit nebo `message` tool nezavolat). Stejná třída problému jako [[Cron job s LLM agentem je nespolehlivý]].
- **Přibalit `message` tool call k existujícímu agent turnu tasku** — žádný extra LLM call, ale stále LLM-mediated; nepokrývá timeout/exception (agent se k toolu nedostane).
- **Patch upstream + nový HTTP endpoint na gatewayi** — čisté řešení (daemon dělá prostý POST, žádný LLM), ale udržovat patch napříč upgrady `nanobot-ai`. Pokud někdy ano, místo je `nanobot/channels/websocket.py` (přidat handler vedle stávajících `/api/sessions/...`, vytvořit `OutboundMessage(channel="websocket", chat_id=..., content=...)` a `bus.publish_outbound(msg)`).
**Rozhodnutí 2026-05-29:** status quo — Telegram fallback stačí, `výsledek <slug>` je dokumentovaný způsob pro WebUI/CLI.
---
## Skill `exec` běží z workspace rootu, ne ze skill adresáře
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/…`.
---
## Python skripty na serveru — uv-native pattern (PEP 723)
Preferovaný způsob pro libovolný stand-alone Python skript v `~/.nanobot/workspace/`:
```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["nanobot-ai", "requests", ...]
# ///
```
`uv run --script` vytvoří a cachuje izolované venv per skript (`~/.cache/uv/`). Skript přežije `uv tool uninstall/install`, upgrade Pythonu i přesun stroje — bez přímé cesty do `~/.local/share/uv/tools/<tool>/bin/python`. První spuštění po vytvoření hlavičky trvá ~5-10s (build venv), další jsou instantní.
**Gotcha pro user systemd:** unit musí mít explicitní PATH, jinak shebang `uv` nenajde:
```ini
[Service]
Environment=PATH=%h/.local/bin:/usr/bin:/bin
ExecStart=%h/path/to/script.py
```
Bez `Environment=PATH` selže s `/usr/bin/env: 'uv': No such file or directory`. Aplikace pravidla na všechny budoucí user systemd unity spouštějící uv skripty (nejen detach).
Zdroj: [PEP 723](https://peps.python.org/pep-0723/), [uv docs `uv run --script`](https://docs.astral.sh/uv/guides/scripts/), ověřeno deployem detach skillu iterace #3.
---
## Systemd `.path` unit s `DirectoryNotEmpty=` — event-driven workspace daemon
Pattern pro libovolný daemon, který má reagovat na soubory v workspace **bez polling**:
```ini
# tasks-daemon.path
[Path]
DirectoryNotEmpty=%h/.nanobot/workspace/tasks/inbox
Unit=tasks-daemon.service
[Install]
WantedBy=paths.target
```
```ini
# tasks-daemon.service
[Service]
Type=oneshot
ExecStart=%h/path/to/daemon.py
```
`%h` = user home. `.path` unit je jen watcher (přes inotify), reálnou akci dělá `.service`. **Level-triggered:** dokud kondice `DirectoryNotEmpty=` platí, systemd po každém doběhnutí service spustí novou instanci. Daemon by měl drenovat celý inbox v jednom běhu (sériově).
Install: `systemctl --user enable --now <unit>.path`. Lingering musí být zapnutý (`loginctl enable-linger nanobot`), jinak user units po odhlášení padnou. Pro reminders se to nepoužívá — ty mají cron výrazy, .path není vhodný (kondice se nemění minutu po minutě). Pro file-driven queue (jako detach) ano.
**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
```ini
[Unit]
StartLimitIntervalSec=1800
StartLimitBurst=20
[Service]
Restart=on-failure
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ý.**
Zdroj: `man systemd.path` + `man systemd.service`, ověřeno smoke testem před deployem detach skillu; latch+hardening history 2026-06-07 19:33.
---
## `nvm` je shell funkce, ne binárka
`nvm` je definován jako bash funkce v `.bashrc` — **není to spustitelný soubor**. Proto ho systemd service nevidí, ani když má správně nastavenou `PATH` s nvm node cestou.
| Příkaz | Typ | Dostupný v systemd service? |
|---|---|---|
| `node`, `npm`, `npx` | skutečné binárky v `.nvm/.../bin/` | ano, pokud je PATH nastavena explicitně |
| `nvm` | shell funkce v `.bashrc` | **ne nikdy** — `.bashrc` se nesourcuje |
Pro správu verzí Node.js z shellu → přihlásit se jako `nanobot` a volat `nvm` interaktivně. Z agenta nebo daemonu → volat `node`/`npx` přímo (fungují přes PATH).
---
## Skill `/keep` — explicit immediate memory
On-demand skill pro okamžitou explicitní paměť. Uživatel řekne „keep X" → agent reformuluje na terse fact → zapíše jako bullet do `workspace/keep.md`. Bez datumů. Dedup, compaction při >150 řádcích.
**Persistent awareness:** `keep.md` není v `BOOTSTRAP_FILES` (ty jsou hardcoded). Trvalé povědomí zajišťuje krátká reference `## workspace/keep.md` na konci `USER.md` (auto-loadovaný každý tah). Skill je tedy čistě write endpoint — neplýtvá context window každou session.
**Kde žije:** `workspace/keep.md` v rootu workspace (vedle `USER.md`, `MEMORY.md`). Edituje ho výhradně `/keep` skill; ostatní agent paths smí číst. **Odděleno od Dream / MEMORY.md** — Dream o `keep.md` neví, needituje ho.
**Dedup pokrývá `keep.md` i `MEMORY.md`:** Write protocol (krok 4) před appendem přečte `workspace/memory/MEMORY.md` a pokud tam je sémanticky podobný fakt (Dream ho mohl destilovat), upozorní uživatele a defaultně přeskočí. `MEMORY.md` je read-only — `/keep` do něj nikdy nezapisuje.
**Ukládá i *why*, ne jen *what* (od 2026-06-06):** Krok 2 Write protokolu rozlišuje typ záznamu — plain fakt (alergie, deploy window, jméno) jde bez důvodu; **rozhodnutí / preference / dead-end** dostane důvod inline na stejném řádku (`<fakt> — because <terse why>`). Pokud je vstup rozhodnutí/dead-end *bez* uvedeného důvodu, model se **jednou doptá** na why (decline/self-evident → uloží bez něj). Záměrně úzká varianta Claude memory.md vzoru, který why přidává jen u feedback/project, ne u reference/faktu. Žádné `Why:` bloky ani few-shot příklady — silné Ollama Cloud / OpenRouter modely zvládnou hranici fakt-vs-rozhodnutí zero-shot. Plný kontext: history.md 2026-06-06.
**Gotcha — BOOTSTRAP_FILES jsou hardcoded:** `nanobot/agent/context.py:25` má `BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"]` — nelze přidat vlastní soubor bez patche. Vše, co má být vidět každý tah bez on-demand loadingu, musí být reference v existujícím bootstrap souboru (USER.md, SOUL.md, …).
---
## Skill `/note` — explicit notes (no auto-load)
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.
**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).
**Kde žije:** `workspace/notes.md`. Edituje ho výhradně `/note` skill. Odděleno od `/keep`, Dream, MEMORY.md — žádný cross-read ani cross-write.
---
## MCP servery v nanobotu — skrytá tokenová zátěž
Každý nakonfigurovaný MCP server přidává do system promptu svůj tool schema popis. Pro sqlite MCP to jsou ~12k tokenů, a to **každý tah** — bez ohledu na to, jestli tool vůbec použiješ.
U menších modelů s omezeným kontextovým oknem (typicky cloud MoE modely s efektivními ~32B params) je to zbytečné plýtvání. Přitom přímá alternativa (CLI `sqlite3` přes `exec`, nebo Python `sqlite3` stdlib přes `uv run`) **tuto zátěž nemá** a pro 95 % use-cases je dostatečná.
**Pravidlo:** MCP server zapojit jen pokud přidaná hodnota nad přímým přístupem výrazně převáží tokenovou cenu. Pro sqlite typicky nepřeváží.
---
## Reasoning stream (``) na konzoli — `channels.showReasoning`
Řádky prefixované ``, streamované token po tokenu (`✻ The`, `✻ user wants`, …) v `nanobot agent` CLI chatu nejsou debug ani chyba — je to **reasoning/thinking stream** modelu. Řídí ho jediný config klíč `channels.show_reasoning` (default `true`, camelCase alias `showReasoning`).
**Vypnout:** `channels.showReasoning = false` v `~/.nanobot/config.json`. Sourozenec `telegram`/`websocket` uvnitř `channels`, ne uvnitř konkrétního kanálu.
- **Je to globální flag, ne per-channel.** Gate čte globální `channels_config.show_reasoning` (`nanobot/cli/commands.py:345,354`), ne per-kanálový config. Nelze vypnout jen pro konzoli a nechat zapnuté ve WebUI — buď všude, nebo nikde. (Trade-off: ve WebUI se reasoning hodí při ladění „proč něco jde/nejde".)
- **Restart:** CLI (`nanobot agent`) čte config čerstvě při startu → stačí restart sezení. Gateway/WebUI/Telegram dostávají `channels` přes `AgentLoop.from_config()` jednou při startu → restart service.
- **Žádný runtime flag** `nanobot agent` na to není; `--logs/--no-logs` řídí jen loguru runtime log, ne reasoning stream.
- Příbuzné knoby v témže bloku: `sendProgress` (default `true`, progress řádky ``), `sendToolHints` (default `false`, tool-call hinty). Vykreslení `` na `commands.py:301`.
Zdroj: `nanobot/config/schema.py:37-39`, `nanobot/cli/commands.py:301,345,354`. Plný záznam: history 2026-06-01 „Vypnutí reasoning streamu".
---
## Context window presetů: default 65k, přepis přes `contextWindowTokens`
Nanobot má **hardcoded default `context_window_tokens = 65_536`** pro `ModelPresetConfig` i `AgentDefaults` (`nanobot/config/schema.py:101,124`). Pokud preset v `config.json` tuto hodnotu nepřepíše, jede model na 65k **bez ohledu na to, co reálně umí**. Klíč v JSON: `contextWindowTokens` (Base má `alias_generator=to_camel` + `populate_by_name=True`, `schema.py:24` → projde camelCase i snake_case). Sourozenec `maxTokens` (max output) má default jen `8192`.
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):**
- **+** 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í.
- Při běžném (nízkém) naplnění se kvalita **nemění** — efekt nastává až když sezení přeroste 65k.
**Otevřená otázka** (todo.md): nenechat slabším modelům kontext spíš na ~128k? Menší okno může dát lepší kvalitu „per token" než maximální naplnění.
---
## bwrap sandbox — limity na bare-metal a jak to funguje v Dockeru
**bwrap bind mounty jsou hardcoded** v `nanobot/agent/tools/sandbox.py` — žádná config volba pro přidání vlastních cest neexistuje. Mountuje se pouze: `/usr` (ro), `/bin`, `/lib`, `/lib64`, `/etc/...` (ro-bind-try), `/tmp` (tmpfs, ephemeral), workspace (rw), media dir (ro). Cesty mimo tyto lokace jsou v sandboxu neviditelné.
**Zamýšlený deployment je Docker** — base image `ghcr.io/astral-sh/uv:python3.12-bookworm-slim` má `uv` i `python` system-wide pod `/usr`; Node.js 20 se instaluje přes `apt` — taky do `/usr`. V kontejneru tedy vše funguje, protože nástroje jsou tam, kde bwrap mountuje.
**Na bare-metal to nefunguje** — `uv` je v `~/.local/bin/uv`, Node/npm v `~/.nvm/.../bin/` — oboje mimo bind mounty. `pathAppend` situaci nevyřeší: přidá cestu do PATH, ale bwrap ten adresář do sandboxu vůbec nenabinduje.
**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):**
- 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
**Žádný jiný sandbox backend než `bwrap` neexistuje** — `_BACKENDS = {"bwrap": _bwrap}`, alternativa je jen `"sandbox": ""` (bez sandboxu).
**Nanobot wiki (nanobot.wiki/docs/0.2.0/) vrací 403** — není veřejně přístupná bez přihlášení.
Zdroj: `nanobot/agent/tools/sandbox.py`, `Dockerfile` (ověřeno 2026-06-05).
---
## 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.
## 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).
---
## `maxTokens` se počítá dvakrát; prompt caching má 3 háčky
**`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:
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.
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.
---
## MiniLoop — změřená čísla `/remind add` parseru (PoC)
Samostatný `.NET` PoC v `src/MiniLoop/` (prompt-only parse text→JSON, `Microsoft.Extensions.AI` nad OpenAI SDK, swap providera přes config). Test `test` mód protáčí 17 párů × všechny modely **souběžně** (modely paralelně, příklady uvnitř modelu sekvenčně; NOW fixní `2026-06-03T14:30:00`). Souběžnost vůči provideru je omezená `maxConcurrency` v configu (`SemaphoreSlim` per provider) — ollama=3 dle kvóty předplatného. Běh 5 modelů s gate=3 (2026-06-03):
| Model | Provider | Úspěšnost | Wall median / avg | Tokeny in / out |
|---|---|---|---|---|
| glm-5.1 | ollama (nvidia.hell) | 17/17 | 1690 / 1927 ms | 21118 / 2896 |
| deepseek-v4-flash | ollama (nvidia.hell) | 17/17 | 4894 / 6118 ms | 21654 / 3487 |
| minimax-m2.7 | ollama (nvidia.hell) | 17/17 | 4815 / 4604 ms | 21969 / 2317 |
| claude-haiku-4.5 | openrouter | 16/17 | 1064 / 1135 ms | 23668 / 691 |
| gpt-5.4-nano | openrouter | 17/17 | 3034 / 4003 ms | 20987 / 553 |
**Hrdlo souběhu = kvóta paralelních dotazů providera, ne sdílený výpočet ani počet spojení.** Ollama předplatné povoluje **max 3 paralelní dotazy** ([ollama.com/pricing](https://ollama.com/pricing)). Když běží víc ollama modelů než 3 naráz, přebytečné dotazy čekají ve frontě a to čekání spadne do wall-clocku (stopky obalují jen HTTP call). Dřív (4 ollama modely bez stropu): glm median 3240 ms, jednotlivá volání qwen až 59 s. Po zavedení `maxConcurrency=3` (a redukci na 3 ollama modely, takže strop zatím ani nepřekáží): glm median zpět na **1690 ms**, žádné odlehlé hodnoty. `SemaphoreSlim` slot se navíc získává **mimo stopky**, takže i kdyby strop překážel, čekání na slot se do měřené latence nezapočte. OpenRouter běží na vlastní infře, strop nemá. Každý model má vlastní `OpenAIClient` (spojení se nesdílí) — víc spojení by nepomohlo.
**Reasoning = pomalé + drahé na out tokeny:** deepseek out=3487, minimax out=2317 — proto ~5 s. (Dříve zavržený qwen3.5 byl extrém: out=22671 tok ≈ jako input, volání i 59 s — proto vyhozen.) haiku/gpt-nano out 550700 tok = přímý parse bez reasoningu. glm rychlý (out~2,9k, ale median 1,7 s).
**FAILy:** jediný „FAIL" haiku = **false negative v test datech** (`zkontrolovat pečení` vs `pečeni`; JsonCompare porovnává `text` přesně, ordinálně). Ostatní modely 17/17. (Z dřívějška: gemma dělala skutečnou chybu data `příští pondělí`→`06-09` místo `06-08`; gemma teď v reálném configu není.)
**Teze PoC potvrzena:** ~1,2k input tokenů na `add` (vs ~2832k přes nanobot agent loop, ~30× méně) a ~1 s wall-clock u rychlých modelů (vs ~10 s u `/remind list` přes agenta). Sedí s odhady z [plans/remind-standalone-bot.md](plans/remind-standalone-bot.md). Plný záznam: history 2026-06-03 „MiniLoop paralelizace".
### Levné / OSS modely z OpenRouteru (změřeno 2026-06-03)
Test 5 levných OpenRouter modelů (cena $/M tok in/out), gate=3 na ollam(ě) se netýká — vše OpenRouter:
| Model | Cena | Úspěšnost | Wall median / avg | out tok |
|---|---|---|---|---|
| `mistralai/mistral-small-3.2-24b-instruct` | 0.075/0.20 | 17/17 | **934 / 1050 ms** ⚠️ | 610 |
| `google/gemma-3-27b-it` | 0.08/0.16 | 17/17 | 1413 / 1611 ms | 608 |
| `z-ai/glm-4-32b` | 0.10/0.10 | 16/17* | 1905 / 2072 ms | 519 |
| `qwen/qwen3-30b-a3b-instruct-2507` | 0.043/0.17 | 16/17* | 1991 / 1866 ms | 588 |
| `openai/gpt-oss-120b` | levný | 16/17 | 7238 / 12164 ms | 3769 |
\* false negative (slovosled `se protáhnout`/`protáhnout se`, resp. `pečení`/`pečeni`).
⚠️ **`mistral-small-3.2` — naměřeno s cache.** Opakovaný test (2026-06-03) ukázal reálné časy 3 4007 400 ms na prvních 4 příkladech, tj. ~38× horší než výsledek výše. Původní 934 ms zřejmě těžilo z cache providera. Skutečná cold performance je cca 45 s median.
**Závěr:** `gemma-3-27b` je spolehlivý OpenRouter kandidát (17/17, 1413 ms). `gpt-oss-120b` propadák — reasoning → 7 s a 6× víc out tokenů. Potvrzení teze o thinkingu: `qwen3-30b-a3b-instruct` 1991 ms vs cloud `qwen3.5` (thinking) 10852 ms + out=22671 — past byl režim thinking, ne qwen. Plný záznam: history 2026-06-03 „MiniLoop levné OSS".
### gemini-flash-lite — nový rekordman OpenRouter (změřeno 2026-06-03)
`google/gemini-3.1-flash-lite` na OpenRouteru:
| Model | Cena | Úspěšnost | Wall median / avg | Tokeny in / out |
|---|---|---|---|---|
| `google/gemini-3.1-flash-lite` | velmi nízká | **17/17** | **683 / 719 ms** | 23285 / 559 |
**Nejlepší výsledek ze všech dosud měřených modelů** — 683 ms median, 17/17, out pouze 559 tok (přímý parse bez reasoningu). Poráží glm-5.1-ollama (1690 ms), haiku-4.5 (1064 ms) i gemma4:e4b local (1136 ms). Plný záznam: history 2026-06-03 „MiniLoop gemini-flash-lite a mistral-small-3.2 cache".
### Malé ollama modely — ministral-3, nemotron-3-nano (změřeno 2026-06-03)
`:cloud` varianty registrované na nvidia.hell přes `POST /api/pull` (cloud pointer, žádný GB download):
| Model | Úspěšnost | Wall median / avg | out tok |
|---|---|---|---|
| `ministral-3:8b-cloud` | 15/17 | 1043 / 1169 ms | 653 |
| `nemotron-3-nano:30b-cloud` | 16/17 | 2015 / 2277 ms | 7805 |
**Ani jeden nepřekonal mistral-small-3.2 — oba zavrženy.** `ministral-3-8b` udělal **skutečnou chybu dne v týdnu** (`každý pátek` → cron `* * 6` sobota místo `* * 5`) — u připomínek vážné, na 8b je to znát; přitom **není ani rychlejší** než mistral-small (1043 vs 934 ms). `nemotron-3-nano-30b`**reasoning sklony (out=7805 tok, ~12× víc než mistral)**, je 2× pomalejší a jeho jediný FAIL byl rozsekání `1,3,5` na tři cron výrazy (rozvrh ekvivalentní, formát ne). Závěr: pod ~24b instruct (mistral-small, gemma-3-27b) klesá spolehlivost cronu a malé „nano" modely buď chybují, nebo zbytečně reasonují. Plný záznam: history 2026-06-03 „MiniLoop ministral/nemotron-nano".
### Lokální gemma4:e4b (změřeno 2026-06-03)
`gemma4:e4b` (8B, 8 GB) — model stažený přímo na nvidia.hell, žádný cloud, žádné náklady:
| Model | Úspěšnost | Wall median / avg | out tok |
|---|---|---|---|
| `gemma4:e4b` (lokální) | **17/17** | **1136 / 1719 ms** | 1530 |
**Překvapivě dobré výsledky pro lokální 8B model.** Median 1136 ms je rychlejší než glm-5.1 cloud (1690 ms) a blízko gemma-3-27b-it na OpenRouteru (1413 ms). Vysoký avg (1719 ms) oproti mediánu (1136 ms) = odlehlé hodnoty u složitějších vstupů (random/multiple times, 34 s). Žádné skutečné chyby, žádný reasoning. out=1530 tok je 2,5× více než mistral-small (610), ale bez reasoningu — model prostě verbosněji okomentuje. **Nejlepší dosud změřený čistě lokální model.** Plný záznam: history 2026-06-03 „MiniLoop gemma4:e4b local".
### Zamítnuté lokální modely
| Model | Důvod zamítnutí | Median | Úspěšnost |
|---|---|---|---|
| `phi4:latest` (14.7B) | 2 skutečné chyby: `příští pondělí``06-05` (čtvrtek!), `dopoledne``09:00` místo `08:00`. Navíc pomalejší než gemma4:e4b | 1744 / 1676 ms | 15/17 |
| `codestral:22b` | Pomalý (2483 ms) + skutečná chyba data: `příští pondělí``06-07` (neděle) místo `06-08` | 2483 / 2621 ms | 16/17 |
| `ministral-3:8b-cloud` | Skutečná chyba weekday v cronu (`pátek` → cron `* * 6` = sobota) | 1043 / 1169 ms | 15/17 |
| `nemotron-3-nano:30b-cloud` | Reasoning sklon (out=7805 tok), 2× pomalejší než mistral-small | 2015 / 2277 ms | 16/17 |
---
## Rychlost: glm-5.1 vs minimax-m3 (Ollama nativní streaming, 2026-06-07)
Měřeno přímo proti Ollamě na `nvidia.hell` (stejný endpoint jako nanobot), streaming `/api/chat`, identický `/remind list` prompt, 3 běhy/model. **`:cloud` modely nevracejí sub-durations** (`eval_duration` ap. = `None`) — tok/s nutno měřit přes streaming (TTFT = čas 1. content chunku).
| Model | TTFT (medián) | Total wall (medián) | Out tok | End-to-end průtok (out/total) |
|---|---|---|---|---|
| glm-5.1 | ~5,9 s | ~7,5 s | 12001730 | **~198 tok/s** |
| minimax-m3 | ~6,8 s | ~10,8 s | 420460 | **~40 tok/s** |
**minimax-m3 je výrazně línější:** TTFT mají srovnatelný (start není problém), ale minimax má **~50 % delší celkovou dobu i přes 34× MÉNĚ vygenerovaných tokenů**. Čistá generace minimaxu ~95120 tok/s (streamuje plynule); glm ~5× vyšší end-to-end průtok. Pozn.: glm „1300 tok/s" z post-TTFT okna NEbrat doslovně — cloud buffer flushne dávku, proto měřit `out/total`. Na interaktivní úkoly je glm-5.1 jednoznačně svižnější. Plný záznam + per-run čísla: history 2026-06-07 „Měření rychlosti glm-5.1 vs minimax-m3".
**Širší rozhodovací rozbor** (GLM-5.1 vs MiniMax M3 vs Kimi K2.6 — kdy který za podmínky Ollama Cloud, capability cliffs, use-case mřížka): [`models.md`](models.md).
### Doplněk: minimax-m2.7 vs glm-5.1 (2026-06-07, prokládaně 5 kol)
`minimax-m2.7:cloud` zmizel z `/api/tags` (Ollama Cloud ho nahradila m3), ale `POST /api/pull` ho dotáhne (cloud pointer). Mediány (cloud byl vytížený → absolutní čísla vyšší než ranní m3 měření, ber jen poměr):
| Model | TTFT | Total wall | Out tok | e2e (out/total) |
|---|---|---|---|---|
| glm-5.1 | 15,4 s | 18,3 s | 1558 | **~91 tok/s** |
| minimax-m2.7 | 9,2 s | 11,3 s | 351 | **~28 tok/s** |
**m2.7 má decode ~3× pomalejší než glm (a horší než m3 ~40 tok/s).** Nižší wall-clock (11 vs 18 s) je **jen díky terseness** (~4,5× méně tokenů), ne rychlejším generováním. Pro delší agentní výstupy (tool args, kód) je pomalý decode handicap. Plný záznam: history 2026-06-07 17:51.
---
## minimax-m3 je pro nanobot agenta nepoužitelný (BLOCKED)
**Verdikt: nenasazovat `minimax-m3` jako agent model.** Vedle pomalosti (~40 tok/s end-to-end, viz sekce výše) má fatální slabinu v **agentní recovery** — neumí přečíst chybovou hlášku toolu a vystoupit ze smyčky.
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`.