Files
nanobot-runtime/develop/knowledge.md

1305 lines
123 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é.
---
## `uv` na serveru není v non-interaktivním SSH PATH
`ssh nanobot@nanobot.hell "uv run ..."` selže s `uv: command not found` — non-login shell nemá `~/.local/bin` v PATH. Plná cesta je `/home/nanobot/.local/bin/uv`. Cron skilly to obcházejí shebangem `#!/usr/bin/env -S uv run --script`. Zdroj: nasazení remind `upcoming` 2026-06-10 (history.md).
**Od nanobotu 0.3.0 platí totéž pro `exec` tool** — viz sekce „Prostředí `exec` toolu" níže. Řeší to `tools.exec.pathPrepend` v `config.json`.
---
## 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".
**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
`~/.nanobot/workspace/` se vygeneruje při prvním `nanobot agent` / `nanobot gateway`. Obsahuje `AGENTS.md`, `USER.md`, `SOUL.md`, `HEARTBEAT.md`, `memory/`, git store. (Do 0.2.x i `TOOLS.md` — od 0.3.0 už ne, viz níže.)
## Co se auto-loaduje do system promptu (verze 0.3.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`. Po editaci **není potřeba restart service** — změna platí od příští zprávy.
- Zdroj: `nanobot/agent/context.py:57` (`BOOTSTRAP_FILES`), `context.py` (`_load_bootstrap_files`).
- **`agent/tool_contract.md` z balíčku** — kontrakt toolů se od 0.3.0 rendruje přímo do promptu (`parts.append(render_template("agent/tool_contract.md"))`), needitovatelný a vždy přítomný. Dřív to byl workspace soubor `TOOLS.md`; upstream commit `d29fcaf5` (21. 5. 2026) ho přesunul do balíčku jako `templates/agent/tool_contract.md`. Pokrývá general tool contract, discovery/`grep`, file workflows, `exec`, CLI apps, web, messaging, scheduling — vlastní verze těchhle témat ve workspace jsou tedy duplicita.
- **`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`~~ | **Od 0.3.0 se nenačítá** — nahradil ho `agent/tool_contract.md` uvnitř balíčku. Náš unikátní obsah (`python — use uv`, `Doručené připomínky`) přesunut do `AGENTS.md` 2026-08-01, soubor smazán. | — | — |
| `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_cli.py delivered [--since YYYY-MM-DD]` dotáhne z `reminder_fires` jen doručené (`status='delivered'`), default dnes; `fire_time`/`delivered_at` se ukládají **Prague-naive**, takže žádná konverze. `TOOLS.md` na to směruje agenta.
**Soft delete:** `remove` nastaví `deleted_at` (záznam zůstane v DB, jen zmizí z `list` a odpalů); hard delete jen přímým DB zásahem. Cron tool se používá pouze pro background agent úlohy, nikdy pro osobní notifikace uživateli.
**Editace DB — vždy přes `remind_cli.py`** (deterministické CLI, SQLite transakce + validace cron/`at`/random), nikdy přímý DB nebo `edit_file`. Volat `uv run skills/remind/scripts/remind_cli.py <subcommand>` (workspace-relativní cesta, exec běží z workspace rootu). Subcommandy: `list`, `add --text … (--cron EXPR… | --at ISO… | --random-times-per-day N --random-window HH:MM-HH:MM [--random-days 1-5] [--random-from DATE] [--random-until DATE])`, `edit --keyword|--id [--text …] [--replace-schedules + nové schedule flagy]`, `remove`, `enable`, `disable`, `delivered`. Výběr záznamu přes `--keyword` (substring; ambiguous → vrátí ids) nebo `--id` (přesný). **Mutace vrací JSON** (`{"added": …}` ap.); **`list` vrací čitelný text** — řádek na reminder `#<id> text [enabled|disabled]` + odsazené schedule řádky (prefix `#<id>` je id pro `--id`), prázdný store → `(no active reminders)`. Chyby stderr + non-zero exit. Env `REMIND_DB` přepíše cestu k DB (testy). **Oprava/změna textu = `edit --id <n> --text "…"`** (id z `list`), NIKDY remove+add (ztratil by se delivery history a změnilo id) — SKILL.md k tomu agenta explicitně navádí. **Plný výčet flagů žije v argparse**, ne v SKILL.md (hybrid od 2026-06-10): `remind_cli.py <cmd> --help` (rename z `remind_edit.py`, history 2026-06-10).
**Gotcha — `log_operation` ignoruje `REMIND_DB`:** audit zápis do `workspace/log/reminder.log` jde přes `__file__`-relativní cestu, **ne** přes `DB_PATH`/`REMIND_DB`. Spuštění test suite **na serveru** proto zapíše fixture texty do reálného `reminder.log` (prod `reminder_fires` zůstává čistá — ta jede přes DB_PATH). **Testy spouštět jen lokálně** (`uv run --with pytest --with croniter pytest skills/remind/tests/`).
**Náhodný (deterministický) čas (`random` blok):** N× denně v náhodný čas uvnitř okna, ale deterministicky — sdílený modul `scripts/random_times.py` počítá časy ze seedu `f"{datum}|{text}"`, takže sender zůstává bezstavový (počítá se každou minutu znovu). Min. rozestup mezi časy = konstanta `MIN_GAP_MIN` (default 15) v tomtéž modulu. Validace v `remind_cli.py` jde přes stejný `compute_fire_times`. (`random_times.py` taky drží sdílené `minutes_to_hhmm` — dřív duplikované v cli i send.) Testy: `uv run --with pytest pytest skills/remind/tests/`. Návrh: [plans/remind-random-time.md](plans/remind-random-time.md).
**Týdenní náhodný režim (`period='week'`, od 2026-06-15):** `--random-times-per-week N` (mutually exclusive s `--random-times-per-day`) rozprostře N odpálení přes týden PoNe na N **různých** náhodných dnů, 1 čas na den uvnitř okna. Engine zůstává **per-day volaný**`_weekly_fire_times` spočítá celý týdenní plán deterministicky ze seedu `f"{pondělí}|{text}|week"` (ne ze dne) a vrátí jen fires daného dne, takže daemon i forecast nemění iterační logiku. Sloupec `schedule_random.period` ('day'/'week', default 'day') + idempotentní `_migrate(conn)` v `db.get_db` (PRAGMA-guarded `ALTER TABLE ADD COLUMN`, protože `init_db` na živou DB nesahá). `count > dostupné dny` se clampuje (hraniční týden u from/until), `count > kapacita filtru` raisuje. Detaily: history 2026-06-15.
**Display IDs (autonomní serverová feature, do repa dotaženo 2026-06-15):** `list`/`upcoming` ukazují **1-based pozici** mezi aktivními remindery (`#1`, `#2`…, počítáno on-the-fly v `_active_display_order`), **ne** interní DB id. `--id` i chybové hlášky (`{"error":"no match","display_id":n}`, `ambiguous` s `display_id`) pracují s display ID. Přečíslovává se po každém `remove` → při nejistotě vždy nejdřív `list`. Interní id se uživateli nikdy neukazuje. (Objeveno při syncu před weekly deployem — feature byla na serveru, ne v repu; pravděpodobně Dream procesor. history 2026-06-15.)
**uv path na serveru:** `/home/nanobot/.local/bin/uv` — není v PATH pro root. Spouštět jako `/home/nanobot/.local/bin/uv run script.py`.
**DB_PATH path (gotcha):** V `remind_send.py`/`remind_cli.py` je `Path(__file__).resolve().parent.parent.parent.parent`**4 levely** nahoru z `.../workspace/skills/remind/scripts/` na workspace root, pak `/db/reminders.sqlite`. Se 3 levely míří cesta mimo (`.../workspace/skills/`) a DB se vytvoří/hledá na špatném místě. Override přes env `REMIND_DB`.
**jobs.json se nepersistuje přes restart agenta:** Změny v `cron/jobs.json` provedené agentem přes `edit_file` tool se mohou ztratit po restartu service (nanobot drží jobs v paměti a přepisuje soubor). Bezpečnější: editovat Python in-place přes SSH + ihned restartovat service.
**Identita reminderu = `id` (SQLite autoincrement).** `text` slouží jako lidský klíč pro `--keyword` (substring match); jednoznačný výběr přes `--id`. Dedup **odpalů** je per `reminder_id`+`schedule_id`. (Historie: per-entry ID bylo nad YAML zvažováno a zavrženo — history 2026-06-02; migrace na SQLite ho zavedla nativně.)
**Aktivní texty jsou unikátní (guard od 2026-07-06).** `add` i `edit --text` **odmítnou** duplicitní text aktivního reminderu (`find_active_by_exact_text`, casefold vč. diakritiky) chybou `{"error":"duplicate text", display_id, hint}` (exit 1). Důvod: agent občas rozdělil „jeden text, víc časů" do víc `add` volání místo jednoho `add` s opakovanými `--at`/`--cron` (doloženo: UFO burger `id=46/47`). Jeden text = jeden reminder s víc schedule. `--id`/`ambiguous` disambiguace zůstává — substring keyword může být nejednoznačný napříč **různými** texty a historické dupy (před guardem) v DB pořád jsou. Plný záznam: history 2026-07-06.
### Vyřešené chyby
**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 `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
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ů:** 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:
- **`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` bylo v `ExecStart` v `~/.config/systemd/user/nanobot.service` na `nanobot.hell`, ale **už tam není** (ověřeno 2026-09-15: `ExecStart=/home/nanobot/.local/bin/nanobot gateway`, poslední `LLM usage` řádek v journalu je z 2026-05-27). Bez `-v` tedy **tokeny nikde nejsou** — `Processing message`/`Response to` jsou INFO a logují se dál, `LLM usage` je DEBUG a ne. 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`.
**Oficiální tvar `description`** ([Anthropic — Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices), platí i pro nanobot, formát je identický). Nanobot wiki frontmatter skillů nedokumentuje — autoritou je tenhle dokument plus zjištění ze zdrojáku výše.
- Tvar: `<co skill dělá, slovesná fráze>. Use when <triggery/kontexty>.` Např. `Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.`
- **Vždy třetí osoba** — `Processes Excel files…`, nikdy `I can help you…` ani `You can use this to…`; nekonzistentní osoba zhoršuje discovery.
- Musí obsahovat **co dělá i kdy použít**, klíčový use case první, konkrétní klíčové termíny. Vágní (`Helps with documents`) je anti-pattern.
- Limit **1 024 znaků** (Agent Skills spec); Claude Code listing ořezává na 1 536.
- Nanobotí dodatek k tomu: „co dělá" = **schopnost**, ne postup. `Adds, deduplicates, and compacts entries in keep.md` je *jak* → do těla.
Aplikováno na `keep` (history 2026-09-04).
## 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`
**Kam Dream smí zapisovat (ověřeno ve zdrojáku 2026-09-02):** `build_dream_tools()` (`nanobot/agent/memory.py:641`) registruje Edit/Write/ApplyPatch s `allowed_dir = workspace/skills` + `extra_write_allowed_files = [memory/MEMORY.md, SOUL.md, USER.md]`. **Číst** smí celý workspace (`ReadFileTool` s `allowed_dir = workspace`).
Dva důsledky:
1. **Do `workspace/projects/` (ani jinam mimo `skills/`) Dream zapsat nemůže** — je to vynucené kódem, ne promptem. Psát takový zákaz do těla skillu je zbytečné (a Dream tělo non-always skillu stejně nečte).
2. **Serverové skilly mohou driftovat proti repu bez našeho zásahu** — Dream do `skills/` zapisovat smí. Doloženo: `skills/project/SKILL.md` na serveru se 2026-09-01 13:15 sám změnil (přibyla sekce o dělbě `prompt.md` vs `state.md`), zatímco repo mělo verzi z 07-22. Stejný jev už dřív u `/remind` display IDs (viz výše). **Proto vždy nejdřív stáhni serverovou verzi a porovnej, než skill přepíšeš.**
---
## 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).
**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` je od 0.3.0 mrtvý klíč.** Upstream commit `dacc6992` (29. 6. 2026) ho vyřadil ze schématu; `_migrate_config()` ho z načtených dat tiše zahodí a zaloguje warning (`… is legacy and ignored; replay max messages is now an internal safety cap`). Replay limit je nově interní safety cap, nekonfigurovatelný. Z našeho `config.json` klíč odstraněn 2026-08-01 (do té doby spamoval journal — 145 výskytů za hodinu). Do 0.2.x platilo: 120 zpráv replay přes `session/manager.py::get_history`.
- **`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)
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.
---
## Exec safety guard shazuje český text — diakritika + dvojtečka = „windowsová cesta"
**Problém → příčina → pravidlo.** `exec` vrátí `Command blocked by safety guard (path outside working dir)` u příkazu, který obsahuje běžnou českou prózu. Příčina: `ExecTool._extract_absolute_paths()` (`agent/tools/shell.py`) hledá windowsové cesty regexem `(?<![A-Za-z])(?:[A-Za-z]:[^\s"'|><;]*|…)`. Lookbehind je **ASCII-only**, takže znak s diakritikou před ASCII písmenem ho neutne — `Cíl:` dá token `l:`, `Závěr:`/`směr:` dají `r:`. `Path("r:").resolve()` to rozvine vůči cwd démona (`/home/nanobot`, ne vůči workdiru příkazu) na `/home/nanobot/r:` → mimo workspace → blok. Ověřeno 2026-09-11 spuštěním nainstalovaného guardu na reálných příkazech.
**Guard jede nad raw command stringem, bez shell parseru** (`_split_shell_segments` se používá jen na allow/deny patterns). Nerozliší tedy argument od obsahu heredocu — heredoc, `--text` i `printf | pipe` s týmž textem padnou identicky. Citování a quoted heredoc **nepomáhají**.
**Pravidlo pro psaní skillů: český text nikdy nedávej do command stringu.** Vždy `write_file` do `tmp/` + předání **cesty** (`--file <path>`, nebo `< tmp/soubor`). Cesta v příkazu je neškodná; relativní cesty žádný z regexů nechytá.
**Pravidlo je od 2026-09-11 zapsané globálně** v `workspace/AGENTS.md`, sekce `## exec Tool` — tedy v system promptu při každém tahu, nezávisle na tom, jaký skill se triggerne. `AGENTS.md` je jediný z `BOOTSTRAP_FILES` (`agent/context.py:57`), který je user-ownovaný; `SOUL.md`, `USER.md` i `memory/MEMORY.md` přepisuje Dream a bundled `templates/` přepíše upgrade balíčku. **Dvě omezení:** (1) `AGENTS.md` se bere z `project_root` aktuálního tahu (`context.py:163`), takže v session scoped do `tmp/<x>` se root verze nenačte — pro běžný chat platí; (2) kdyby byl obsah identický s `templates/AGENTS.md`, soubor se do promptu tiše nedá (`context.py:179182`). Tentýž guard byl v `AGENTS.md` předtím zdokumentovaný **dvakrát izolovaně** (sekce `## Git commit timestamps` o `date '+%H:%M:%S'` → token `H:%M:%S`, a původní věta o chybějícím workspace path), aniž by se pojmenovala společná příčina.
Co blokuje a co ne (ASCII písmeno + `:`, kde znak **před** písmenem není ASCII písmeno):
| Blokuje | Projde |
|---|---|
| `Cíl:`, `Závěr:`, `směr:`, `díl:` | `Úkol:`, `Stav:`, `Řešení:`, `Otázky:`, `Poznámka:` |
Pozor i na druhý guard nad raw stringem: `if "..\\" in cmd or "../" in cmd` → jakákoli **prozaická** zmínka `../` shodí příkaz na `path traversal detected`.
**Stav skillů.** Opraveno na `--file`: `project` (`log --file`) a `note` (`note_capture.py --file`, 2026-09-11 — byl nejrizikovější, protože bere vstup uživatele doslova, takže poznámka „Cíl: …" tiše selhala). **Zbývá dluh:** `bookmark` (český článek v heredocu) a `remind` (`edit --text "…"`). Upstream regex zůstává rozbitý — fix by chtěl unicode-aware lookbehind.
---
## Prostředí `exec` toolu — PATH z procesu tam nedosáhne
`ExecTool._build_env()` (`agent/tools/shell.py`) staví prostředí subprocessu **od nuly**. Na Unixu předá jen `HOME`, `LANG`, `TERM`, `PYTHONUNBUFFERED` (+ cokoli v `tools.exec.allowedEnvKeys`). **`PATH` se z `os.environ` nekopíruje** — na Windows ano, na Unixu ne. Cokoli nastavíš v systemd unitu, `~/.profile` nebo wrapperu, `exec` neuvidí.
Do 0.2.2 to nevadilo, protože `exec` běžel jako **login shell** (`bash -lc`) a ten sourcoval `~/.profile` s `PATH="$HOME/.local/bin:$PATH"`. Upstream commit `13c951aa` (25. 6. 2026) přepnul default `login` na `False` — kvůli secrets, které se profilem vracely zpátky do prostředí. Bez login shellu se PATH dopočítá z **vestavěného defaultu bashe**:
```text
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.
```
`~/.local/bin` tam není → `uv: command not found`, exit 127.
**Jediná správná cesta je `tools.exec.pathPrepend` / `pathAppend` v `config.json`.** Hodnota musí být **adresář**, ne cesta k binárce — `/home/nanobot/.local/bin/uv` do PATH lookupu nepřispívá ničím. Implementace: `_wrap_path_export()` předřadí příkazu `export PATH="$NANOBOT_PATH_PREPEND:$PATH"; …`.
**Preferuj `pathPrepend`.** Default bashe končí `.` (aktuální adresář) a `exec` běží s CWD = workspace root, do kterého zapisuje agent i Dream — s `pathAppend` by `.` bylo v pořadí **před** našimi cestami.
**Není hot-reload.** Config watcher volá `agent.invalidate_runtime_config()` (`agent/loop.py:516`), což invaliduje jen model-runtime resolver; `ExecTool.create(ctx)` běží jednou při startu gateway. Po změně `tools.exec.*` je nutný `systemctl --user restart nanobot`.
**Protiváha — PATH procesu inertní není.** `run_cli_app` spouští CLI aplikace s `env=os.environ.copy()` (`apps/cli/service.py:1372`) a MCP stdio servery dědí taky. Proto `Environment=PATH=` v systemd unitu zůstává — jen neřeší `exec`.
Zdroj: `agent/tools/shell.py` (`_build_env`, `_wrap_path_export`, `_prepare_command`), upstream commit `13c951aa`. Diagnóza a oprava: history 2026-08-01.
---
## 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á workspace-relativní `skills/remind/scripts/remind_cli.py` (korektní); detach analogicky `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:57` má `BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md"]` (do 0.2.x i `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` — osobní znalostní báze (capture → compile, od 2026-07-01)
Přepsáno z SQLite row-store na **capture → compile pipeline** (vzor llm-wiki/detach, ale lehčí: jeden dokument, žádný index/graph/lint). Plná historie: history 2026-07-01. Plán: [plans/note-prepis.md](plans/note-prepis.md).
- **Úložiště (bez DB):** `notes/notes.md` = JEDEN strukturovaný dokument s tematickými sekcemi (`##`), které řídí LLM. Fronta zachytů = soubory v `notes/inbox/`, po zpracování → `notes/done/`, zadržené (paywall/nečitelné) → `notes/hard/`. Audit každého zachytu v `log/note.log`.
- **Dual-mode:** `/note <text>` = **okamžitě** (capture + inline compile v témže tahu, default); `/note cron <text>` = **odloženě** (jen capture, zpracuje cron). Oba volají `note_capture.py` (dumb atomic zápis) + sdílený *compile workflow* v SKILL.md + sdílený `notes/.compile.lock`.
- **Compile:** `note_compile.py` (systémový crontab uživatele `nanobot`, každou minutu) — levný fs pre-check bez importu nanobota, lockfile, `Nanobot.from_config().run(DRAIN_GOAL)` (process_direct bez cron preamble). Reformuluje na terse fakta, URL stáhne přes `web` tool + detekce paywallu, zařadí do sekce, přesune zdroj do `done/`/`hard/`.
- **Search:** `/note search|find <dotaz>` → načti celý `notes.md`, odpověz (read-only). Mazání = editace dokumentu (žádné display ID, žádné tagy).
- **Odděleno** od `/keep`, `MEMORY.md`, Dream, llm-wiki (`cml/`). **Kolize triggerů s `keep`:** NL „poznamenej si/ulož si …" může spadnout do `keep` (obě to claimují) — spolehlivý je explicitní `/note` prefix; description note skillu odlišen („NOT durable facts → keep"). Starý `note.py`/`note.sqlite` vyřazeny (`note.sqlite` v `backup/note-retired/`).
---
## Skill `/project` — pojmenované dlouhodobé pracovní kontexty (přepsáno 2026-07-22, skript 2026-09-02)
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.
**Zápis do `memory.md` jde výhradně přes `skills/project/scripts/project_cli.py log`** (od 2026-09-02). Subcommandy `activate` / `log` / `list` / `new`; volat workspace-relativně `uv run skills/project/scripts/project_cli.py …`. Skript vlastní datum (systémové hodiny, Europe/Prague) a koncový newline — obojí model prokazatelně kazil: `chata/memory.md` měl dva záznamy s vymyšleným datem `2026-09-14` (zapsané 09-01) a `proxmox/memory.md` slepený append na předchozím řádku, protože `edit_file` kotva se hádala bez přečtení souboru. Text se předává **výhradně souborem**: `write_file` do `tmp/` + `log <slug> --file tmp/…` (od 2026-09-11). Dřív to byl stdin quoted heredoc (`<<'NOTE'`), protože `--text` model prokazatelně mrzačil (v `log/note.log` je doložený případ, kdy argument zmršil na `...`) — jenže heredoc padl na exec safety guardu, který sejme jakýkoli český text v command stringu bez ohledu na citování (viz sekce „Exec safety guard shazuje český text" výše; doloženo session `002f2196`, kdy tři varianty po sobě selhaly na `Diagnóza/směr:`). `--text` je proto z CLI **odstraněn**, stdin zůstal jako fallback. Stav skriptu přepisuje env `PROJECTS_DIR` (testy). Plný kontext: history.md 2026-09-02.
**Projektová data nemají strop ani konsolidaci** — `memory.md` roste neomezeně a nikdy se nekomprimuje ani nearchivuje. Velikost se řeší **výhradně na straně čtení**: `activate` nad limitem tool výsledku vypustí z výstupu nejstarší záznamy a ukáže cestu k plnému logu, soubor na disku nechá beze změny. Zamítnutá varianta: prahy 8 000 / 12 000 znaků s nabídkou konsolidace — ztráta zadaného obsahu je horší failure mode než jakákoli úspora kontextu (a čísla stála na špatném okně, viz níže).
## `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).
---
## 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.
**Gotcha — `agents.defaults.contextWindowTokens` je zavádějící číslo.** V serverovém `config.json` je `65536`, ale to platí jen když preset vlastní hodnotu nemá. Reálné okno aktuálního defaultu (**`glm53`: 976 000**, `glm52`/`glm-flash` 976 000, `kimi3` 1 020 000, `sonnet`/`gemini-flash` 256 000) je ~15× větší, než `defaults` napovídá. **Než z 65k něco odvodíš, přečti `model_presets`, ne `agents.defaults`** — na tomhle jsem 2026-09-02 postavil celý (zamítnutý) rozpočet velikosti pro `/project`. Limit, který v praxi kouše, je `maxToolResultChars: 16000`, ne okno.
**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).
**Korekce (2026-07-02): na produkčním `nanobot.hell` je sandbox VYPNUTÝ, takže pip závislosti přes `uv run` fungují.** Server má v `config.json` `exec` blok `"sandbox": ""` + `pathAppend` na `~/.local/bin/uv`, takže `exec` běží bez bwrap — stejné prostředí jako plain SSH. Skilly reálně jedou se závislostmi (`remind`→croniter, `llm-wiki`→pyyaml) a `bookmark`/`html_to_markdown.py` používá `trafilatura` (v2.1.0 ověřeno naživo: `uv run --with trafilatura` stáhne a naimportuje). Limity výše platí jen pro nasazení se *zapnutým* bwrap (`"sandbox": "bwrap"`), typicky Docker. Zdroj: live test 2026-07-02, viz `history.md` téhož dne.
---
## Rozpad input contextu (co zabírá tokeny každý tah)
Detailní naměřený rozpad ~15k baseline input contextu (system prompt po částech + tool definitions 18 toolů) je v samostatném souboru [`tokens-explain.md`](tokens-explain.md) — k 0.2.1, preset glm-5.1. Stručně: ~8,7k system prompt (největší `MEMORY.md`, `skills_section`, `SOUL.md`), ~5,2k tool defs, zbytek session zprávy.
## llm-wiki: zaseknutí způsobuje LLM v mechanické smyčce, ne pomalost
Při wiki lint/fix operacích se agent zasekl proto, že dělal **mechanickou práci v LLM smyčce** (čte stránku → hledá problém → edituje → další stránka). Správné řešení přesouvá tuto práci do deterministického skriptu — LLM rozhoduje jen tam, kde je skutečně potřeba úsudek.
**Praktické důsledky pro llm-wiki:**
- Lint musí vracet `(soubor, řádek, návrh opravy)` — ne jen "9 broken refs" → agent nemusí číst žádnou stránku
- `--fix` mód má smysl jen pro mechanické triviality (chybějící frontmatter pole ze šablony) kde není úsudek
- Slug collision a frontmatter šablona patří do compile pipeline, ne do lintu ex post
**Zamítnuté návrhy ze stejného důvodu:** timeout na každý krok, "aspoň jeden edit po každém čtení" (tlačí na editaci), agentní wiki fix subcommand (opět smyčka). Viz todo.md sekce "llm-wiki skill — další zlepšení".
---
## Optimalizovat skilly kvůli tokenům se nevyplatí
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`.
## /remind: model nevypisuje celou tabulku → mezera v instrukcích, ne bug skriptu
**Problém → příčina → fix:** Modely (kimi27 i default) po `/remind list` dostanou kompletní tabulku (CLI exit 0), ale jeden ji scvrkne na počet, druhý si ji přerenderuje po svém. → Příčina: `SKILL.md` sekce `## Behavioral contract` *popisovala formát* výstupu, ale neříkala „předej kompletní" — modely to čtou jako surová data k vlastnímu formátování. → Fix: odstavec **Showing read results** (`list`/`upcoming`/`delivered` = user-ready text, vypsat každou položku s `#display-id`, nesumarizovat). Plný záznam: history 2026-06-15 13:21.
**Diagnostika chování nanobot agenta = číst webui session logy.** Konverzace (user/assistant/tool turny, včetně `reasoning_content` a `exec` výstupů) žijí na serveru ve `workspace/sessions/websocket_<id>.jsonl` (kopie i v `~/.nanobot/webui/`). Pro „proč model udělal X" stáhnout příslušnou session a číst turny — odhalí, že příkaz uspěl a chyba je až v prezentaci.
## Session logy: 89 % objemu jsou tool výsledky
Z 534 souborů v `~/.nanobot/workspace/sessions/` je 332 reálných konverzací (12 311 zpráv, 14,1 MB obsahu). **12,5 MB (89 %) tvoří návratové hodnoty toolů** — už oříznuté na `maxToolResultChars: 16000`. Pro analýzu chování stačí nahradit je metadaty (`name(args) → ok|ERROR, velikost`), čímž korpus spadne na ~3 MB. Přírůstek je 4,4 session/den, aktivita jen 70 % dní. Zdroj: měření 2026-09-01, `skills/reflect/scripts/reflect_distill.py`.
## `reflect`: nálezy platí pro okno, ne pro celou historii (2026-09-02)
Dokud běh dohání backlog chronologicky, nálezy popisují **nejstarší** nezpracované session,
i když je předkládá jako aktuální. Reálně: 3 běhy zpracovaly 56 z 356 session, všechny
z 26.29. 5., a všech 8 rozhodnutých nálezů (4 aplikované do `SOUL.md`) tak opravovalo
chování z konce května. Fix: `since = max(cursor, now - window)` — cursor je **podlaha**
(nic dvakrát, jinak `merge_findings` sečte počty znovu), okno **strop** (starší session se
přeskočí natrvalo). Default `--window-days 21`. Plný záznam: [history.md](history.md) 2026-09-02.
Dávkování samo nálezy nezkreslovalo — každý z těch tří běhů byl jedna dávka.
## `reflect`: destilát session vyrostl z ~5 kB na ~14 kB (měřeno 2026-09-02)
21denní okno = 74 session = **6 dávek** po 200 kB (~1 MB destilátu). Původní plán počítal
s ~4,4 session/den po ~5 kB, tedy jednou dávkou. Důsledek: `--deadline-minutes 20` +
`TIMEOUT_SECONDS 45 min` zvládnou 12 dávky za noc, takže okno se dohání ~2,4 dne za noc.
Při dimenzování dávkovaného běhu nad session logy je tedy nutné velikost **měřit**
(`reflect_distill.py --stats`), ne odvozovat z počtu session.
## `reflect`: počty v nálezu jsou self-report modelu, ne měření
`occurrences` / `sessions_affected` si vymýšlí analyzující LLM a `merge_findings` je jen
sčítá napříč běhy. Nekoherenci to propustilo do store: nález `f09a7` měl `occurrences: 4`
a `sessions_affected: 5`. Skript umí zkontrolovat jen invarianty
(`sessions_affected ≤ min(occurrences, počet session v dávce)`, clamp ve validátoru) —
absolutní hodnota zůstává nedokázaná a v review se nesmí prezentovat jako měření.
## `reflect`: dávkovaná analýza nevidí rozptýlené vzory
Prompt zakazoval hlásit jednorázový slip, takže vzor s frekvencí ~1× na dávku se nikdy
nepojmenoval → nikdy nespočítal → práh `≥2 výskyty a ≥2 session` nepřelezl. Práh sám
skládání napříč dávkami zvládá; chyběl mu vstup. Fix: prompt smí hlásit jediný výskyt
vzoru, který **už je v „Known patterns"**, a dostal informaci, že vidí jen výsek historie.
Novost vzoru se tím nemění — genuinely nový singleton se pořád nehlásí.
## `reflect`: zápis do auditního store patří výhradně skriptu
`SKILL.md` říkal agentovi, aby u nálezu bez patche „propose the exact old_text/new_text
yourself", ale `reflect_apply.py` patch přijmout neuměl (`--new-text-file` jen přepíše
`new_text` **už existujícího** patche). Agentovi nezbylo než editovat `findings.jsonl`
ad-hoc skriptem — session `7a988478` na to spotřebovala 3 jednorázové skripty. Fix
(2026-09-02): akce `--set-patch <json>`, která kandidáta nejdřív prožene `check_patch()`
a **teprve pak** zapíše, plus věta ve STOP gate 4, že se store needituje ručně nikdy.
Poučení obecně: dokud pro nějaký legitimní krok neexistuje volání skriptu, prompt ho
nezakáže — agent si cestu najde a bude mít pravdu.
## `reflect`: „naposledy" u nálezu byl dřív datum přepsání záznamu
`_seen_line()` vydávalo `created` (kdy se záznam naposledy složil) za „last seen", takže
nález refilovaný každou noc hlásil dnešek bez ohledu na stáří důkazů. Ze stejného zdroje
plynuly falešné regrese: `regression_of` se nastavilo, kdykoli se vzor po `applied` objevil,
i když analyzovaná session byla z doby **před** opravou (`retry-without-diagnosis` 67×
označen REGRESE, nejnovější důkaz 31. 8., patch 1. 9.). Fix (2026-09-02): derivované pole
`last_seen` = `max(evidence[].when)` (jen tvary `^\d{4}-\d{2}-\d{2}`, prvních 10 znaků,
fallback `created`) — a `regression_of` jen když `last_seen > applied.at[:10]`, jinak `watch`.
`when` je volný string od modelu a formáty se míchají (`2026-08-31` i `2026-08-31 13:53`).
## `reflect`: report počítal nálezy po dávkách, ne po vzorech
`_run()` dělalo `merged += batch_findings`, takže vzor nalezený v 5 dávkách byl v `merged`
5× — včetně mezistavů, které fold zahodil. Z toho žil report, `stats` i Telegram: report
z 2. 9. měl **29 nadpisů proti 13 vzorům ve store**. Fix (2026-09-02): `merged` je
`dict[str, Finding]` klíčovaný `pattern`, poslední zápis (nejvíc složený) vyhrává.
Store byl správně po celou dobu — nafouknutá byla jen prezentace.
## Vyřešené chyby ve skillu `reflect` (2026-09-02) — noční běh nedobíhal
Noční cron padal na 30min timeout a zahazoval i to, co už měl hotové:
- **All-or-nothing zápis** → `_write_findings`, kurzor i report se dělaly po poslední dávce,
takže timeout zahodil dvě dávky s platnými nálezy a kurzor nechal na místě → další noc totéž
plus nové session → fix: `commit()` po **každé** dávce, kurzor se nikdy nepohne dozadu.
- **Žádný strop na délku běhu** → 412 session za kurzorem = 7 dávek ≈ 90 min proti 30 min
→ fix: soft deadline `--deadline-minutes` (default 20), `TIMEOUT_SECONDS` 45 min už jen jako
brzda na zaseknutou dávku.
- **185k-tokenový prompt přetékal provider timeout** (`_OPENAI_COMPAT_REQUEST_TIMEOUT_S` = 120 s,
`NANOBOT_LLM_TIMEOUT_S` = 300 s), retry zahodí hotový prefill a začne znovu → 9 timeoutů za běh
→ fix: `os.environ.setdefault` na 600/900 s + dávka 500 kB → 200 kB (~70 k tokenů).
- **Infra chyba šla do JSON validátoru** → nanobot vrací `Error calling LLM: …` jako odpověď
agenta, validátor to hlásil jako „invalid JSON" a spotřeboval 1 ze 3 pokusů, model dostal
vytýkáno něco, co nenapsal → fix: `RunResult.stop_reason == "error"` se pozná dřív, přepošle
se **původní** prompt v čerstvé session, vlastní strop 2 pokusy.
Efekt: dávka z ~13 min na **147 s**, jedna iterace, žádný retry. Plný záznam:
[history.md](history.md) 2026-09-02.
## `RunResult` z `Nanobot.run()` rozlišuje selhání providera
`nanobot/sdk/types.py:50``RunResult``stop_reason` a `error`. Když LLM call selže, nanobot
vrátí text chyby jako **obsah odpovědi** (`Error calling LLM: timed out after 300s`) a zároveň
nastaví `stop_reason="error"` + `error` (`agent/runner.py:289-305`, `agent/loop.py:1019`).
Skript, který parsuje odpověď agenta, musí tenhle stav testovat **před** validací — jinak
diagnostikuje infra výpadek jako vadný výstup modelu.
## Timeouty LLM callu v nanobotu: 120 s na request, 300 s na celý call
Dvě vrstvy, obě přebitelné env proměnnou:
| Vrstva | Default | Env | Zdroj |
|---|---|---|---|
| jeden HTTP request na OpenAI-kompatibilní endpoint | **120 s** | `NANOBOT_OPENAI_COMPAT_TIMEOUT_S` | `providers/openai_compat_provider.py:89,163` |
| celý call včetně retry (3 pokusy) | **300 s** (0 = vypnuto) | `NANOBOT_LLM_TIMEOUT_S` | `agent/runner.py:702` |
| idle mezi chunky ve streamu | 90 s | `NANOBOT_STREAM_IDLE_TIMEOUT_S` | `providers/base.py:20` |
SDK cesta (`Nanobot.run()`) **nestreamuje** (`stream: False`), takže idle timeout se jí netýká.
Retry počítá prefill znovu od nuly — u desítek tisíc tokenů promptu je tedy dražší než čekání,
proto se u dávkových skriptů vyplatí request timeout zvednout, ne zkracovat.
## `reflect`: `--new-text-file` bez `--check` aplikuje okamžitě (2026-09-02)
`reflect_apply.py --new-text-file <path>` **není náhled** — bez `--check` jde přímo do
`apply_finding()`, tedy zápis souboru + commit. Náhled uživatelovy editace je až
`--check --new-text-file`, a protože se jeho text do `patch` neukládá, musí temp soubor
přežít do aplikace a předat se znovu. `SKILL.md` to mělo v tabulce rozhodnutí zaměněné
(řádek `edit:` sliboval diff, předepisoval aplikaci) — opraveno, viz history 2026-09-02 14:55.
## Vyřešené chyby ve skillu `reflect` (2026-09-01)
Čtyři tiché chyby — nic nespadlo, jen se dělo něco jiného, než co slibovala dokumentace:
- **Kurzor nefiltroval** → `_run` ukládal zformátovaný stamp (`2026-07-11 14:02`), `collect_sessions`
ho porovnávala proti syrovému ISO (`…T09:00:…`); `'T'` > `' '`, takže 9 session z dne kurzoru se
analyzovalo znovu každý běh a výskyty se počítaly dvakrát → fix: `SessionDigest.started` drží syrové
ISO, formátuje se až v hlavičce.
- **Zamítnutí vydrželo jen jeden běh** → `merge_findings` brala `previous` jako záznam s nejnovějším
`created`, takže `watch` záznam založený po zamítnutí přebil `rejected` → fix: zamítnutí je vlastnost
vzoru (set přes všechny záznamy), ne posledního záznamu. Stejná příčina ztrácela `regression_of`.
- **`reflect_apply.py` hlásil „refused" po zápisu** → soubor se přepisoval před commitem, selhání
`git commit` vrátilo exit 2 se změněným souborem → fix: rollback na původní obsah, který
`check_patch` už vracel.
- **Validátor zahazoval celou dávku** kvůli neznámému klíči nebo o 10 znaků delší diagnóze → retry
přeposílal ~420k tokenů → fix: retry jen při neparsovatelném JSON nebo když nepřežil ani jeden nález.
Plný záznam: [history.md](history.md) 2026-09-01.
## Test může projít kolem chyby, když si vstup vyrobí v jiném formátu než volající
`test_since_excludes_already_processed_sessions` podával `since` jako `"2026-08-01T00:00:00"` — formát,
který produkční kód nikdy nevyrobí (ukládal `"2026-08-01 00:00"`). Test byl zelený a chyba běžela
v produkci. **U hodnot, které jedna část kódu zapisuje a druhá čte, testuj round-trip, ne literál.**
Zdroj: `skills/reflect/tests/test_reflect_distill.py`, history 2026-09-01.
## Tokenizace nanobot korpusu: ~1,2 znaku na token
Naměřeno na reálném destilátu (90 kB promptu → 75 135 tokenů podle tiktoken v `nanobot.agent.memory`). Čeština, názvy toolů a UUID se tokenizují špatně. **Odhad „34 znaky na token" je pro tenhle korpus 3× mimo** — kdo počítá velikost dávky, musí použít 1,2.
## `contextWindowTokens` presetu přebíjí `agents.defaults`
`agents.defaults.contextWindowTokens` je 65536, ale preset `glm53` má 976000 a **vyhrává**`agent/loop.py:476` (`context_window_tokens = extra.pop(...) or resolved.context_window_tokens`). Efektivní input budget = `contextWindowTokens - maxTokens - 1024` (`SNIP_SAFETY_BUFFER`, `agent/context_governance.py:105`), tedy ~958k tokenů pro glm53.
## Velká zpráva projde, `snip_history` krátí jen historii
`ContextGovernor.snip_history` (`agent/context_governance.py:383`) zahazuje **celé starší zprávy**, nikdy nekrátí jednu zprávu — a nejnovější zprávu přidá vždy, i když sama překročí budget (`if kept and kept_tokens + msg_tokens > remaining_budget: break`, řádek 422). Velký vstup je proto lepší poslat **přímo ve zprávě** než souborem přes `read_file`, který by ho uřízl na `maxToolResultChars` (16 000 znaků).
## `uv` není v PATH neinteraktivního SSH
`ssh nanobot@nanobot.hell 'uv run …'` skončí `failed to run command 'uv': No such file or directory`. Je v `/home/nanobot/.local/bin/uv` — přes SSH je nutná plná cesta. **Crontab si `PATH` nastavuje sám** (`PATH=/home/nanobot/.local/bin:/usr/bin:/bin`), tam bare `uv run` funguje. Souvisí s `tools.exec.pathPrepend` (history 2026-08-01).
## LLM rozbíjí JSON českou uvozovkou — a hláška o tom musí být konkrétní
GLM-5.3 při psaní české diagnózy do JSON stringu napsal `(„repeated external lookup blocked")`: otevírací uvozovka je `„` (U+201E), ale **zavírací je ASCII `"`**, která neescapovaná ukončí string. Generická hláška „no parseable json block" nedá retry nic použitelného a všechny 3 pokusy selžou stejně. Fix: validátor hlásí `msg`, řádek, sloupec a výřez textu okolo `error.pos`, plus prompt zakazuje uvozovky uvnitř string hodnot. Po opravě prošel první pokus. Zdroj: `skills/reflect/scripts/reflect_auto.py:_decode_payload`, history 2026-09-01.
## `~/.nanobot/workspace` je git repo a nanobot do něj commituje sám
Lokální repo **bez remote**; 15 z posledních 20 commitů je `nanobot <nanobot@dream>` (Dream dělá `dream: periodic memory consolidation`). `.gitignore` vynechává `db/ sessions/ log/ tmp/ backup/ tasks/ cron/runs/`. Důsledek: skill, který mění soubory na serveru, nepotřebuje vlastní zálohy — commit před změnou a `git revert` stačí. Pozor: **není to záloha mimo stroj** a je to repo nespojené s `src/nanobot`.
## Stav skillu nesmí bydlet v adresáři skillu
`rsync -av skills/<name>/ …` přepisuje celý adresář, takže `state.json` nebo databáze uvnitř `skills/<name>/` se při nasazení ztratí. Data patří do vlastního adresáře v rootu workspace — vzor `skills/note/` + `notes/`, nově `skills/reflect/` + `reflect/`.
## `reflect`: odhad šance opravy stojí na tom, jestli je text v kontextu (2026-09-02)
`/reflect` ukazuje u nálezu odhad `~80 / ~60 / ~40 / ~20 %`, že oprava vzor skutečně
zastaví. Hlavní osa rubriky není kvalita formulace, ale **jestli je opravovaný text
v kontextu ve chvíli, kdy chyba vzniká**: gate ve skriptu drží vždy (~80 %), tvrdý zákaz
v `SOUL.md`/`AGENTS.md`/`SKILL.md` dotčeného skillu ~60 %, přeformulování tamtéž ~40 %,
soubor mimo kontext nebo ponechání na úvaze agenta ~20 %. `regression_of` sráží o pásmo —
instrukce toho druhu už na tom vzoru jednou selhala. Pásma, ne přesná čísla: je to odhad
ze záznamu, ne měření. Zdroj: `skills/reflect/SKILL.md` sekce „Estimating the odds",
history 2026-09-02.
## `reflect`: `--workspace` umožní zkoušet zásahy mimo ostrý store (2026-09-02)
`reflect_apply.py --workspace <cesta>` bere kompletní workspace odjinud, takže se dá
`--set-patch`/`--check`/`--apply` vyzkoušet na kopii (`findings.jsonl` + cílový soubor
v `tmp/`) a ostrý store i cílový soubor zůstanou nedotčené. Použito při ověření draftování
patche na reálném nálezu `f5c34`. Zdroj: `skills/reflect/scripts/reflect_apply.py:250`,
history 2026-09-02.
## `keep.md` je v kontextu každého tahu — proto je jeho čistota load-bearing (2026-09-04)
`AGENTS.md``## Explicit user details` říká *„Explicit user facts are stored in `keep.md`.
**Read at every turn**"*. Co v `keep.md` leží, platí model každý tah. Dva praktické důsledky:
pravidlo pro agenta uložené v keep se čte jako *fakt o uživateli*, ne jako instrukce (slabší
tah než `AGENTS.md`), a referenční materiál tam obchází `/note`, kde ho uživatel hledá přes
`/note search`. Do keep patří jen trvalý fakt / preference / rozhodnutí o uživateli; ostatní
routuje `## Triage` v `skills/keep/SKILL.md`. Zdroj: history 2026-09-04.
## `notes/` v serverovém workspace repu commituje výhradně skill `note` (2026-09-04)
`skills/note/SKILL.md`*Versioning*: Dream se `notes/` nedotýká, takže `note` je jediný,
kdo tam commituje — a vždy jen `git add notes/`, nikdy `git add -A` (zbytek workspace vlastní
Dream). Zbytek serverového workspace repa běžně stojí s necommitnutými změnami
(`keep.md`, `AGENTS.md`, `cron/jobs.json`, `reflect/*`) — to je normální stav, ne rozbité repo.
Při ruční editaci `notes.md` proto commitni s prefixem `note:` a stage jen `notes/`.
Zdroj: history 2026-09-04.
## Ollama Cloud embeddingy neexistují — cloud tier je completion-only (2026-09-09)
Žádný z 18 cloud modelů nemá capability `embedding`; `/api/embed` na cloud modelu vrací
`unauthorized`, **zatímco `/api/generate` na tomtéž modelu projde** — takže to není problém
autorizace, cloud tier ten endpoint prostě neobsluhuje a hláška je zavádějící. Katalog na
ollama.com to potvrzuje: filtr `c=embedding` vrací 12 modelů, všechny jen ke stažení, žádný
s cloud tagem. Embeddingy je tedy nutné hostovat lokálně. Zdroj: history 2026-09-09.
## Cena brute-force cosine v SQLite je z 96 % Python režie, ne matmul (2026-09-09)
Nad 10⁵×1024 float32: **594 ms = read 382 + pack 190 + dot 22**. Samotný numpy matmul je
zanedbatelný; platí se za extrakci BLOBů a jejich složení do matice. Proto `sqlite-vec`
(`vec0`) dá **19 ms @ 10k a 212 ms @ 100k** — skenuje v C. `.npy` + `mmap_mode='r'` je
ještě rychlejší (35 ms), ale za cenu druhého souboru mimo DB. Pozor: `vec0` **není ANN
index** — lineární škálování 19 → 212 ms to potvrzuje, výhoda je konstanta, ne asymptotika.
Zdroj: history 2026-09-09.
## FTS5 a čeština: `remove_diacritics` + prefix wildcard stačí, trigram netřeba (2026-09-09)
`unicode61 remove_diacritics 1` a výš foldí diakritiku (dotaz `zaloha` najde `záloha`;
hodnota `0` ne). Stemming `unicode61` neumí, ale **prefix wildcard to pokryje**: `záloh*`
i `zaloh*` najdou *záloha, zálohování, zálohy*. Dotazová vrstva tedy lepí `*` na termy —
trigram tokenizer není potřeba. Souvisí s varováním v `remind/scripts/store.py`
(`find_active_by_exact_text()`), že SQLite `lower()` foldí jen ASCII. Zdroj: history 2026-09-09.
## `vec0` není cílem foreign key — kaskáda ho nevyčistí (2026-09-09)
`ON DELETE CASCADE` z nadřazené tabulky uklidí navázané řádky **i FTS5 external-content
tabulku** (AFTER DELETE trigger se na kaskádě spustí), ale řádek ve `vec0` virtuální tabulce
**osiří** — indexer ho musí mazat explicitně. Jinak index při mazání souborů tiše plní mrtvými
vektory. Ověřeno smoke testem; `DELETE`/re-`INSERT`/`UPDATE` po `rowid` a `ROLLBACK` ve `vec0`
jinak fungují normálně (sqlite-vec v0.1.6). Zdroj: history 2026-09-09.
## Ollama `keep_alive` musí být číslo, ne string (2026-09-09)
`{"keep_alive": -1}` i `{"keep_alive": "24h"}` → HTTP 200; **`{"keep_alive": "-1"}` → HTTP 400**.
Cold load `qwen3-embedding:0.6b` (639 MB) je 1,81 s vs. warm 0,043 s, takže připíchnutí modelu
se vyplatí, ale není kritické. Batching je naopak podstatný: 8,9 chunk/s po jednom vs.
**108 chunk/s v batchi 32**. Zdroj: history 2026-09-09.
## Instruct prefix je u `qwen3-embedding` load-bearing, ne kosmetika (2026-09-09)
Na 12 parafrázových dotazech nad 123 reálnými chunky: **recall@10 8/12 s prefixem
vs. 3/12 bez něj** (MRR 0,261 vs 0,204). Plán `final-wiki-hybrid-rag` ho označoval za
„nekritickou vlastnost" na základě 5/5 vs 5/5 na jiném korpusu — na uživatelských datech
to neplatí. Prefix musí být **bitově identický** při indexaci i dotazu, proto žije
v `meta.query_prefix` a jeho změna vyžaduje `wiki_sync.py --full`. Zdroj: history 2026-09-09.
## FTS5 prefix wildcard NEfolduje českou flexi — jen prefixy (2026-09-09)
`záloh*` najde *záloha/zálohování/zálohy* jen proto, že `záloh` je **kmen**. Se skutečně
skloněným dotazovým slovem to selže: naměřeno `cestu*`**0** chunků v
`travel/packaging-list.md`, `cesty*` → 3, `cest*` → 4. Wildcard je prefixový, takže pomůže
jen když je dotazové slovo prefixem tvaru v dokumentu — a česká flexe mění koncovku, ne
začátek. Důsledek: dotaz „co si vzít na cestu do zahraničí" nedostal doslovný seznam věcí
na cestu ani do top 10. Tohle je polovičnost D6 v plánu, ne chyba implementace.
Zdroj: history 2026-09-09.
## Hybrid RRF zlepšuje rank, ne pokrytí (2026-09-09)
Nad reálnými poznámkami: MRR **0,367 u RRF** vs 0,257 (BM25 sám) a 0,261 (vektory samy) —
merge dvou ranků téže množiny opravdu vyhrává. Ale `recall@10` u RRF je **6/12**, zatímco
vektory samotné 8/12: RRF řadí podle **shody** obou polovin, takže chunk, který našla jen
vektorová polovina na ranku #9, vytlačí z top-10 chunky, na kterých se poloviny shodnou.
Očekávaný kompromis, ne vada. Zdroj: history 2026-09-09.
## Bare `python3` neotevře `vec_chunks` — potřebuje `sqlite_vec.load()` (2026-09-09)
Dotaz na běžné tabulky (`chunks`, `files`) přes systémový `python3` projde, ale jakmile se
sáhne na `vec0` virtuální tabulku, přijde `sqlite3.OperationalError: no such module: vec0`.
Extension se musí načíst explicitně (`enable_load_extension(True)` + `sqlite_vec.load(conn)`),
což skill dělá v `wiki_db.get_db()`. Při ruční inspekci indexu na serveru je proto nutné
`uv run --with "sqlite-vec==0.1.6"`. Zdroj: history 2026-09-09.
## `ty.toml` `extra-paths` je plochý jmenný prostor — kolize modulů mezi skilly (2026-09-09)
Typechecker `ty` řeší `import store` proti seznamu `extra-paths` a **vyhrává první cesta**,
takže dva skilly se stejným jménem modulu si navzájem rozbijí kontrolu (testy `wiki`
dostávaly `store` z `remind`). Proto všechny skilly kromě `remind` prefixují moduly jménem
skillu (`note_capture.py`, `wiki_sync.py`) — je to nutnost, ne estetika. U kolize, které se
nelze vyhnout (`wiki_search.py` je i v retired `llm-wiki`), rozhoduje **pořadí** v
`extra-paths`. Zdroj: history 2026-09-09.
## Ollama usage API: co v odpovědi je a co ne
`GET https://ollama.com/api/usage` (Bearer klíč z `workspace/.env`) vrací
`limits.session` (hodinové okno) a `limits.weekly`, každé s `usage` (zlomek limitu)
a `models: [{name, request_count}]`**pole, ne slovník**. `activity.cost` je na Pro
plánu rozbité (vždy `0.00000`). Žádné reset timestampy, žádné tokeny, žádný cost split.
**`usage` má 3 desetinná místa** (`0.072`, `0.156`) → rozlišení 0,1 % limitu.
Delta za minutu je proto skoro vždy 0 — jediná přesná veličina je `request_count`
per model. Ověřeno 2026-09-15.
## Nanobot nemá nikde per-session spotřebu tokenů
- `memory/history.jsonl` = destilát paměti z Dream procesoru (`cursor`, `timestamp`,
`content`, u části `session_key`) — **není** to účtovací log, žádné tokeny.
- `sessions/*.jsonl` = per-turn zprávy (`role`, `content`, `timestamp`, `tool_calls`,
`reasoning_content`, `latency_ms`) — **taky bez tokenů a bez názvu modelu**.
- Jediný zdroj tokenů je `LLM usage: prompt=… completion=… cached=…` v journalu, což je
**DEBUG** (vyžaduje `-v` v `ExecStart`, viz sekce o log levelu) a navíc nenese session id.
→ Atribuce spotřeby na session jde jen **časovou korelací** (timestampy v `sessions/*.jsonl`
a `Processing message from …` v journalu) proti řadě vzorků z `db/ollama_usage.sqlite`.
Ověřeno 2026-09-15, zdroj: [[plans/ollama-usage-poller.md]] sekce Revize.