# 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 ` 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//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 ` 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 `, `/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 ` 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 ` (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 `# text [enabled|disabled]` + odsazené schedule řádky (prefix `#` 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 --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 --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 Po–Ne 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":"","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\"][\"\"] = {\"provider\": \"\", \"model\": \"\"} 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 `. **Konvence pojmenování presetů:** krátký alias podle modelu — `kimi`, `kimi27`, `kimi3`, `glm`, `glm52`, `sonnet`, `haiku`, `gemini-flash`. (Dřív tu stálo `-` 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":""}'` → `model_info[".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` přidáno do `ExecStart` v `~/.config/systemd/user/nanobot.service` na `nanobot.hell`. Logy živě: `ssh nanobot@nanobot.hell 'journalctl --user -u nanobot.service -f --no-pager'`. **Co `-v` ukáže v jednom tahu** (ověřeno na WebUI zprávě): - `Processing message from :: ` — příchozí zpráva - stavy agentního tahu: `RESTORE → COMPACT → COMMAND → BUILD → RUN → SAVE → RESPOND` (každý s časem) - `Tool call: ({...args...})` — **volání toolu i s argumenty** (INFO) - `LLM usage: prompt=… completion=… cached=…` — spotřeba tokenů každé iterace agentní smyčky - `Response to :: ` — 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 … ` (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 | | 6–7 | **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//` 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//` ze zdroje (např. plugin `.claude-plugin/skills//`) do `~/.nanobot/workspace/skills//` — žá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: `- **** — \`cesta/k/SKILL.md\``. Tělo SKILL.md se načte **až on-demand**, když si agent skill sám přečte přes`read_file`. Důsledky: - `description` je jediná info o skillu v promptu, dokud agent nečte tělo → patří tam jen *kdy/proč* skill spustit (trigger fráze, odlišení od příbuzných skillů), **ne** *jak* funguje. - `description` se **nezkracuje** (`_get_skill_description` vrací text doslova) a je v promptu **každý tah** u všech skillů → trvalý token cost. Drž stručně, routing-orientovaně. Detailní postup patří do těla. - **Always skilly** (`metadata.nanobot.always: true`): `description` se **ignoruje úplně**, do promptu se eager vkládá **celé tělo** (bez frontmatteru). Druhý vysvětlující odstavec v `description` je u nich čistý šum. **Co tedy patří do `description`:** jen *kdy/proč* skill spustit — krátká věta o účelu + trigger fráze + případné odlišení od příbuzného skillu. **Nepatří** tam *jak* skill funguje (to do těla, čte se on-demand) ani detailní postup. Triggery nemusí být dvojjazyčné — model rozpozná záměr napříč jazyky, takže explicitní CZ varianty nic nepřidají, jen prodlužují řádek (ověřeno na `/plan`, 2026-05-31). Zdroj: `nanobot/agent/skills.py:111-159` (`build_skills_summary`, `_get_skill_description`), `skills.py:94-109` (`load_skills_for_context`, always skilly), `nanobot/agent/context.py:87-95`. ## Dream procesor — automatické self-improvement Nanobot má vestavěný Dream procesor (`agent/memory.py:Dream`) který běží každé 2 hodiny. Jde o **dvou-fázový LLM pipeline** nad `history.jsonl`: - **Fáze 1:** Plain LLM call analyzuje historii, hledá fakta (`[MEMORY]`/`[USER]`/`[SOUL]`), kandidáty na smazání (`[FILE-REMOVE]`), opakující se workflow (`[SKILL]`) - **Fáze 2:** AgentRunner s `read_file`/`edit_file`/`write_file` tools provede chirurgické editace; umí sám vytvářet nové skilly (`skills//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: ` a `Chat 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: ``` 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: \n\n"`, 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:`), 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:")` 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 ""` → 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= source=`. 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 " → `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 ` (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é (10–30 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 ` je dokumentovaný způsob pro WebUI/CLI. --- ## 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//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//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 .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 .service .path` + `restart .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 (` — because `). 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 ` = **okamžitě** (capture + inline compile v témže tahu, default); `/note cron ` = **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 ` → 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//{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/.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á **stdin quoted heredocem** (`<<'NOTE'`), ne `--text` — shell obsah neinterpretuje, takže `„"`, `'` i `"` projdou doslova (v `log/note.log` je doložený případ, kdy model `--text` argument zmršil na `...`). 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="")` 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 ~1–2k 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 (~40–75 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ítek–stovek 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 550–700 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 ~28–32k 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 400–7 400 ms na prvních 4 příkladech, tj. ~3–8× horší než výsledek výše. Původní 934 ms zřejmě těžilo z cache providera. Skutečná cold performance je cca 4–5 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` má **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, 3–4 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 | 1200–1730 | **~198 tok/s** | | minimax-m3 | ~6,8 s | ~10,8 s | 420–460 | **~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 3–4× MÉNĚ vygenerovaných tokenů**. Čistá generace minimaxu ~95–120 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_.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 1–2 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 `, 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` má `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 ` **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 „3–4 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 ` (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// …` přepisuje celý adresář, takže `state.json` nebo databáze uvnitř `skills//` 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 ` 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.