915 lines
89 KiB
Markdown
915 lines
89 KiB
Markdown
# 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).
|
||
|
||
---
|
||
|
||
## Kdy je a není potřeba restart nanobot.service
|
||
|
||
**Restart NENÍ potřeba:**
|
||
|
||
| Soubor | Proč |
|
||
|---|---|
|
||
| `~/.nanobot/workspace/cron/jobs.json` | Cron service volá `_load_store()` při každém ticku — soubor se načte znovu automaticky |
|
||
| `~/.nanobot/workspace/reminder.yaml` | Čte ho `remind_check.py` jako subprocess; každé spuštění čte čerstvě |
|
||
| Skripty v `workspace/skills/` | Exec tool je spouští jako subprocess pokaždé znovu |
|
||
| `~/.nanobot/config.json` — **providers a modelPresets** | `_refresh_provider_snapshot()` volá `load_config()` před každým agentem tahem; `/model` přepínač funguje okamžitě |
|
||
|
||
**Restart JE potřeba:**
|
||
|
||
| Soubor / změna | Proč |
|
||
|---|---|
|
||
| `~/.nanobot/config.json` — channels, tools, MCP servery, workspace | Tyto sekce se předávají do `AgentLoop.from_config()` jednou při startu |
|
||
| `~/.config/systemd/user/nanobot.service` | Po změně: `daemon-reload` + restart |
|
||
|
||
**Zdroj:** `nanobot/agent/loop.py:_refresh_provider_snapshot()`, `nanobot/cron/service.py:_load_store()`, `nanobot/providers/factory.py:load_provider_snapshot()`
|
||
|
||
---
|
||
|
||
## Restart nanobot.service jako root
|
||
|
||
`systemctl --user restart nanobot.service` jako root **selže** — user bus není dostupný bez správných env proměnných. Správný postup:
|
||
|
||
```bash
|
||
su - nanobot -s /bin/bash -c '
|
||
XDG_RUNTIME_DIR=/run/user/$(id -u nanobot)
|
||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u nanobot)/bus
|
||
systemctl --user restart nanobot.service
|
||
'
|
||
```
|
||
|
||
**Pozor:** `kill -HUP <pid>` na gateway proces nanobot **nezrestartuje** — proces se ukončí a systemd ho nenaskočí zpět (není to watchdog). Místo HUP vždy používej `systemctl --user restart`.
|
||
|
||
---
|
||
|
||
## Cron joby nanobota: jobs.json
|
||
|
||
Naplánované joby jsou v `/home/nanobot/.nanobot/workspace/cron/jobs.json`. Struktura: pole `jobs`, každý má `id`, `schedule` (kind=`cron`/`every`/`at` s `expr`/`every_ms`/`at_ms`, volitelně `tz`), `payload` (kind=`agent_turn`, `message`, `channel`, `to`, `channelMeta`, `deliver`), volitelně `deleteAfterRun` (true pro `at` joby = jednorázové).
|
||
|
||
Změna se projeví **bez restartu** — cron service volá `_load_store()` při každém ticku (`nanobot/cron/service.py:394`), jobs.json se čte čerstvě. Hot reload tedy funguje out-of-box.
|
||
|
||
**Editace:** Python in-place editor přes SSH, např.:
|
||
|
||
```bash
|
||
ssh root@nanobot.hell "python3 -c \"
|
||
import json; from pathlib import Path
|
||
p = Path('/home/nanobot/.nanobot/workspace/cron/jobs.json')
|
||
data = json.loads(p.read_text())
|
||
# ... uprav data ...
|
||
p.write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
||
\""
|
||
```
|
||
|
||
---
|
||
|
||
## Cron job s LLM agentem je nespolehlivý pro "pošli jen když něco je"
|
||
|
||
Nanobot cron job **vždy** běží přes agenta (`agent.process_direct`) — neagentní typ jobu neexistuje. Dva problémy v cestě prázdného výstupu:
|
||
|
||
1. **Prompt je obalený natvrdo v kódu.** `nanobot/cli/commands.py:on_cron_job` přilepí před `payload.message` fixní `"The scheduled time has arrived. Deliver this reminder to the user now…"`. Tvoje "exit silently" instrukce je s tím v konfliktu → agent improvizuje meta-odpověď ("Output was empty…").
|
||
2. **`evaluate_response` je fail-open.** `nanobot/utils/evaluator.py` rozhoduje o doručení druhým LLM callem; při chybě / chybějícím tool-callu vrací `True` (doruč). Slabší modely často `"no tool call returned, defaulting to notify"` → meta-odpověď propadne na Telegram. Proto únik jen "sem tam" a pokaždé jinak formulovaný.
|
||
|
||
**Zamítnuto:** pouhá úprava promptu na "exit silently" (nestačí — viz body 1+2).
|
||
**Fix:** doručování úplně mimo agenta — viz `/remind skill` níže (system crontab + přímé Bot API).
|
||
|
||
Plný rozbor: history 2026-05-27 "Spam Output was empty".
|
||
|
||
**Pozn. (2026-07-15):** Popis výše (`on_cron_job` hardcoded preamble + `evaluate_response` fail-open) platí pro starší nanobot. V `nanobot-ai` 0.2.2 je mechanismus jiný — viz "Cron `agent_turn` joby v 0.2.2 — `is_bound_cron_job`" níže. Závěr (LLM cron job pro non-reminder úlohy je nespolehlivý → řešit mimo `jobs.json`) platí dál, jen jinou cestou.
|
||
|
||
---
|
||
|
||
## Cron `agent_turn` joby v 0.2.2 — `is_bound_cron_job`, `session_key` nezávislý na doručení
|
||
|
||
V nainstalovaném `nanobot-ai` 0.2.2 nahradil starý "hardcoded reminder preamble + evaluate_response" mechanismus (viz sekce výše, teď zastaralá popiskem) nový: `dream` a `heartbeat` jsou speciální system joby v `on_cron_job` (`nanobot/cli/commands.py`); libovolný jiný `agent_turn` job z `jobs.json` jde přes `is_bound_cron_job()` (`nanobot/cron/session_turns.py`) → `run_bound_cron_job()` (`nanobot/cron/bound_runner.py`).
|
||
|
||
**`is_bound_cron_job(job)`** vrací `True`, když `payload.kind == "agent_turn"` a má `session_key`+`origin_channel`+`origin_chat_id`, a **žádné** z `deliver`/`channel`/`to`/`channel_meta` není nastavené. Bound joby se spustí přes `agent.submit_cron_turn(...)` **jako normální session turn** — **bez jakéhokoli delivery gatingu** (na rozdíl od `heartbeat`, který volá `evaluate_response(..., default_notify=False)` s fail-closed chováním). Cokoli agent v bound cron tahu odpoví, jde přímo do kanálu.
|
||
|
||
**`session_key` (kam se zapíše historie) je nezávislý na `origin_channel`/`origin_chat_id` (kam se doručí)** — `origin_delivery_context()` (`nanobot/cron/session_delivery.py`) čte výhradně `payload.origin_channel`/`origin_chat_id`, vůbec ne `session_key`. Lze tedy nastavit `session_key` na cokoli (jiná/izolovaná session, nebo — pokud jde mimo `jobs.json` — per-run unikátní hodnota) a doručení do daného kanálu/chatu zůstane funkční.
|
||
|
||
**Gotcha:** `jobs.json` neumí dynamický/per-run `session_key` (je to statický string v definici jobu) — takže "vždy čerstvá session" se **nedá** vyřešit uvnitř `jobs.json`, i když by teoreticky šlo nastavit fixní odlišnou session (izolovanou od živého chatu, ale pořád akumulující historii mezi běhy). Pro opravdu čistý kontext při každém běhu je potřeba jít mimo `jobs.json` úplně — Python API `Nanobot.run(session_key=…)` s novou hodnotou (např. timestamp) při každém spuštění, vzor `detach` (`f"detach:{path.stem}"`). `Nanobot.run(ephemeral=True)` (`nanobot.py:126-135`, `process_direct(ephemeral=True)` interně) existuje, ale neprochází channel-delivery pipeline — po ephemeral běhu by bylo nutné doručení řešit ručně (publish `OutboundMessage` nebo přímé Bot API volání).
|
||
|
||
**Praktický důsledek:** Cron job navázaný `session_key` na **stejnou session jako živý chat uživatele** (`f"{channel}:{chat_id}"`, typicky vznikne, když se job zakládá přímo z chatu) kontaminuje conversation historii mezi denním chatem a nočními běhy navzájem — omyl agenta v jednom běhu se táhne do dalších. Řešeno pro `compact-memory-auto-daily`: history 2026-07-15.
|
||
|
||
Zdroj: `nanobot/cron/session_turns.py:is_bound_cron_job`, `nanobot/cron/bound_runner.py:run_bound_cron_job`, `nanobot/cron/session_delivery.py:origin_delivery_context`, `nanobot/nanobot.py:126-165` (`Nanobot.run`).
|
||
|
||
---
|
||
|
||
## Workspace vzniká při prvním spuštění agenta
|
||
|
||
`~/.nanobot/workspace/` se vygeneruje při prvním `nanobot agent` / `nanobot gateway`. Obsahuje `AGENTS.md`, `USER.md`, `SOUL.md`, `HEARTBEAT.md`, `TOOLS.md`, `memory/`, git store.
|
||
|
||
## Co se auto-loaduje do system promptu (verze 0.2.0)
|
||
|
||
**Každý tah** ContextBuilder skládá system prompt z těchto zdrojů (žádná cache, fresh `read_text()`):
|
||
|
||
- **Bootstrap files** v rootu `~/.nanobot/workspace/`: `AGENTS.md`, `SOUL.md`, `USER.md`, `TOOLS.md`. Po editaci **není potřeba restart service** — změna platí od příští zprávy.
|
||
- Zdroj: `nanobot/agent/context.py:25` (`BOOTSTRAP_FILES`), `context.py:156` (`_load_bootstrap_files`).
|
||
- **`memory/MEMORY.md`** — hardcoded cesta v `MemoryStore`. **Žádný jiný soubor v `memory/` se NEčte** (ani `.bak`, ani user-vytvořené `.md`). `history.jsonl` konzumuje výhradně Dream procesor.
|
||
- Zdroj: `nanobot/agent/memory.py:55` (`memory_file = memory_dir / "MEMORY.md"`), `memory.py:205,229`.
|
||
- **Skilly s `metadata.always: true`** ve frontmatteru `workspace/skills/<name>/SKILL.md` — přes `SkillsLoader.get_always_skills()`. Ostatní skilly se nahrávají on-demand, ne do system promptu.
|
||
- Zdroj: `nanobot/agent/skills.py:203`.
|
||
|
||
**HEARTBEAT.md není v system promptu každého tahu** — má vlastní mechanismus přes `heartbeat/service.py`, čte se jen na heartbeat tick (default 30 min).
|
||
|
||
**Důsledek:** Když agent v chatu vytvoří soubor v `memory/` mimo `MEMORY.md` (např. `memory/film_policy.md`), tváří se jako že si pravidlo „uložil", ale **agent ho v dalším tahu neuvidí**. Místo toho ho musí jít do bootstrap souboru — viz následující sekce.
|
||
|
||
## K čemu slouží jednotlivé workspace soubory
|
||
|
||
| Soubor | Doména | Co tam patří | Co tam nepatří |
|
||
|---|---|---|---|
|
||
| `SOUL.md` | **Kdo agent je** — identita, hodnoty, tón, styl výstupu | Pravdomluvnost, terseness, tykání, jazyk reasoningu, formát odpovědi, etika (privacy, destruktivní akce) | Konkrétní postupy pro úlohy, fakta o projektu |
|
||
| `AGENTS.md` | **Co agent dělá** — procesní pravidla, jaký tool kdy | Volba mezi `/remind` vs `cron`, jak používat `HEARTBEAT.md`, varování typu „nepiš reminder do MEMORY.md" | Identita, hodnoty, fakta o uživateli |
|
||
| `USER.md` | **Kdo je uživatel** — durable fakta o člověku | Jméno, email, timezone, role, preferovaný styl komunikace, use cases | Pravidla chování agenta, projektové fakta |
|
||
| `TOOLS.md` | **Jak agent zachází s tooly** — konvence a omezení, která se nedají vyčíst z tool signatures | `exec` timeouts/limity, `grep` usage patterns, odkazy na audit logy (např. `log/reminder.log`) | Globální chování (to je SOUL), procesní pravidla (to je AGENTS) |
|
||
| `memory/MEMORY.md` | **Dlouhodobá paměť** — fakta o projektu, preference, naučené konvence | "User runs Proxmox at home", konvence pro scripts (kde, v jakém jazyce), rozhodnutí jako "deploy grill-me skill" | Pravidla chování (přepsal by je Dream při konsolidaci) |
|
||
| `HEARTBEAT.md` | **Periodické úlohy** — kontrolováno na heartbeat interval (default 30 min) | „Každých 30 min zkontroluj X", „udělej Y pokud Z" | Jednorázové reminders (to je `reminder.yaml` přes `/remind`) |
|
||
|
||
**Test umístění** (rozhoduj podle otázky, ne podle obsahu pravidla): „Mění to **kdo jsem** (SOUL) / **co dělám** (AGENTS) / **kdo je uživatel** (USER) / **jak používám tool** (TOOLS) / **co vím o projektu** (MEMORY) / **co dělám pravidelně** (HEARTBEAT)?"
|
||
|
||
Zdroj: upstream `nanobot/templates/{AGENTS,SOUL,USER}.md` (header docstrings), `nanobot/agent/context.py`, `nanobot/agent/memory.py`, `nanobot/heartbeat/service.py`.
|
||
|
||
## Ollama provider potřebuje `/v1` suffix v `apiBase`
|
||
|
||
Nanobot volá **OpenAI-kompatibilní `/v1/chat/completions`**, ne Ollama-native `/api/chat`. V configu musí být `apiBase: http://host:11434/v1` — bez `/v1` vrací Ollama 404.
|
||
|
||
`docs/configuration.md` to v příkladu (`http://localhost:11434`) **neuvádí** — je to zavádějící.
|
||
|
||
## modelPresets = jeden agent, víc modelů
|
||
|
||
Nanobot **nepodporuje víc pojmenovaných agentů**. Místo toho má `modelPresets` — pojmenované dvojice `(provider, model)`, mezi kterými se přepíná za běhu příkazem `/model <preset>` v chatu (Telegram i WebUI). Default je `agents.defaults.modelPreset`.
|
||
|
||
## Gateway s `websocket.host: 0.0.0.0` bez tokenu odmítne start
|
||
|
||
Bezpečnostní pojistka — pokud má WebSocket channel `host: 0.0.0.0` (bind všech rozhraní), vyžaduje vyplněný `token`. Jinak gateway selže při startu.
|
||
|
||
## Porty gateway
|
||
|
||
| Port | Co tam je |
|
||
|---|---|
|
||
| **8765** | WebUI HTML (SPA) + WebSocket auth endpoint na stejném portu |
|
||
| **18790** | Gateway health endpoint (`/health` → `{"status":"ok"}`) |
|
||
|
||
## CLI chat mód: `nanobot agent`
|
||
|
||
Interaktivní konverzace s agentem přímo v terminálu se spouští příkazem `nanobot agent`. Je to stejný agent jako přes Telegram/WebUI a sahá do stejného `~/.nanobot/workspace/` (sdílí paměť, bootstrap soubory i git store). Fungují v něm i slash-příkazy (`/model <preset>`, `/restart`, `/history`, `/status`, `/goal`, …).
|
||
|
||
Přehled CLI módů: `nanobot onboard` (setup wizard), `nanobot agent` (chat v terminálu), `nanobot gateway` (WebSocket gateway pro WebUI/Telegram).
|
||
|
||
Zdroj: upstream HKUDS/nanobot Quick Start („3. Chat: `nanobot agent`").
|
||
|
||
## `/model` bez argumentu vypíše dostupné presety
|
||
|
||
V chatu (CLI `nanobot agent` / Telegram / WebUI) napsání samotného `/model` (bez argumentu) vypíše status: aktuální model, aktuální preset a seznam dostupných presetů. `/model <preset>` přepne. Stejný seznam se ukáže i při pokusu přepnout na neexistující preset.
|
||
|
||
Seznam ukazuje **nakonfigurované `modelPresets`** z `~/.nanobot/config.json`, ne katalog modelů, co provider reálně nabízí (na to viz Ollama `…/api/tags`, OpenRouter `…/api/v1/models`). OpenAI-kompatibilní endpoint `/v1/models` vrací taktéž jen presety.
|
||
|
||
Zdroj: `nanobot/command/builtin.py` (`cmd_model`, `_model_command_status`).
|
||
|
||
## Telegram bot commands — `/new` resetuje session, `/restart` ne
|
||
|
||
V Telegramu jsou slash-příkazy zaregistrované jako **`BotCommand`** (objeví se v menu po stisku `/` v inputu). Nejsou to volné texty pro agenta — regex router (`_forward_command`) je posílá rovnou do AgentLoop, agent je v promptu nevidí.
|
||
|
||
| Příkaz | Co dělá |
|
||
|---|---|
|
||
| **`/new`** | **Reset session.** Zruší aktivní task, vyprázdní zprávy v sessionu, snapshot pošle do Consolidatoru na archivaci na pozadí. Tohle je „clear context" před novou diskuzí. |
|
||
| `/restart` | Restartuje **bota (proces)**, ne session — po restartu konverzace pokračuje. Slouží k načtení nové konfigurace, ne k čistění kontextu. |
|
||
| `/stop` | Zruší aktuálně běžící task, kontext nechá. |
|
||
| `/history` | Vypíše posledních N zpráv (read-only). |
|
||
| `/status`, `/goal`, `/pairing`, `/model`, `/dream`, `/dream_log`, `/dream_restore`, `/help` | Ostatní registrované commands. |
|
||
|
||
Pozn. k aliasům: Telegram nepovoluje pomlčku v command jménu, takže `/dream_log` a `/dream_restore` jsou aliasy — handler je interně přemapuje na kanonické `/dream-log` a `/dream-restore` (`_normalize_telegram_command`).
|
||
|
||
Zdroj: `nanobot/channels/telegram.py:258-326` (BotCommand registrace, regex router, alias normalizace), `nanobot/command/builtin.py:199` (`cmd_new` — `session.clear()` + background `consolidator.archive(snapshot)`).
|
||
|
||
## /remind skill — architektura a gotchas
|
||
|
||
**Úložiště = SQLite `~/.nanobot/workspace/db/reminders.sqlite`** (dřív `reminder.yaml`; migrace 2026-06-10). Schema: `reminders` (id, text, enabled, timezone, created_at, updated_at, deleted_at) + tři schedule tabulky `schedule_at`/`schedule_cron`/`schedule_random` (FK na reminder, cascade) + `reminder_fires` (audit jednotlivých odpalů: schedule_type, fire_time, delivered_at, status, error_message). Doručuje **systémový crontab uživatele nanobot** (každou minutu), který spouští `skills/remind/scripts/remind_send.py` přes `uv run` — čte DB, porovnává cron / `at` / random s Prague časem, při shodě posílá **přímo přes Telegram Bot API** (token z `config.json`, chat_id z `channels.telegram.allowFrom[0]` s fallback konstantou). Žádný agent, žádný LLM. Deduplikace přes tabulku `reminder_fires` (klíč reminder_id + schedule_id + schedule_type + fire_time, status='delivered'). `log/reminder.log` je teď provozní/debug log **všech operací** (ADD/EDIT/…/DELIVER, UTC) — ne zdroj pravdy pro doručení (viz `delivered` níže); `log/reminder_cron.log` zachytává stdout/stderr crontabu (zdravý běh = prázdný).
|
||
|
||
**Proč mimo agenta:** dřív to byl nanobot cron job `remind-check` přes agenta — spamoval "Output was empty" kvůli fail-open evaluatoru (viz výše "Cron job s LLM agentem"). Crontab to obchází deterministicky.
|
||
|
||
**Nesahej na to přes cron tool:** nikdy nevytvářet `remind-check` job v `cron/jobs.json`. Doručování řeší crontab mimo nanobot.
|
||
|
||
**Nevytvářet ani agentní delivery skill** (např. `deliver-reminder-notifications`, který by exec-em volal nějaký `remind_check.py`). Žádný takový skript v `remind/scripts/` není — je tam jen `remind_send.py` volaný cronem. Migrace na deterministické doručování ho udělala zbytečným. Pokud takový skill ve `workspace/skills/` najdeš, je to mrtvý zbytek a smaž ho.
|
||
|
||
**Telegram:** `/remind text` je bot command, nedojde k agentovi jako text. Psát přirozeně: `připomeň mi...`, `nastav připomínku...`
|
||
|
||
**Read-back doručených („co dnes přišlo?") = subcommand `delivered`**, ne čtení logu. `remind_cli.py delivered [--since YYYY-MM-DD]` dotáhne z `reminder_fires` jen doručené (`status='delivered'`), default dnes; `fire_time`/`delivered_at` se ukládají **Prague-naive**, takže žádná konverze. `TOOLS.md` na to směruje agenta.
|
||
|
||
**Soft delete:** `remove` nastaví `deleted_at` (záznam zůstane v DB, jen zmizí z `list` a odpalů); hard delete jen přímým DB zásahem. Cron tool se používá pouze pro background agent úlohy, nikdy pro osobní notifikace uživateli.
|
||
|
||
**Editace DB — vždy přes `remind_cli.py`** (deterministické CLI, SQLite transakce + validace cron/`at`/random), nikdy přímý DB nebo `edit_file`. Volat `uv run skills/remind/scripts/remind_cli.py <subcommand>` (workspace-relativní cesta, exec běží z workspace rootu). Subcommandy: `list`, `add --text … (--cron EXPR… | --at ISO… | --random-times-per-day N --random-window HH:MM-HH:MM [--random-days 1-5] [--random-from DATE] [--random-until DATE])`, `edit --keyword|--id [--text …] [--replace-schedules + nové schedule flagy]`, `remove`, `enable`, `disable`, `delivered`. Výběr záznamu přes `--keyword` (substring; ambiguous → vrátí ids) nebo `--id` (přesný). **Mutace vrací JSON** (`{"added": …}` ap.); **`list` vrací čitelný text** — řádek na reminder `#<id> text [enabled|disabled]` + odsazené schedule řádky (prefix `#<id>` je id pro `--id`), prázdný store → `(no active reminders)`. Chyby stderr + non-zero exit. Env `REMIND_DB` přepíše cestu k DB (testy). **Oprava/změna textu = `edit --id <n> --text "…"`** (id z `list`), NIKDY remove+add (ztratil by se delivery history a změnilo id) — SKILL.md k tomu agenta explicitně navádí. **Plný výčet flagů žije v argparse**, ne v SKILL.md (hybrid od 2026-06-10): `remind_cli.py <cmd> --help` (rename z `remind_edit.py`, history 2026-06-10).
|
||
|
||
**Gotcha — `log_operation` ignoruje `REMIND_DB`:** audit zápis do `workspace/log/reminder.log` jde přes `__file__`-relativní cestu, **ne** přes `DB_PATH`/`REMIND_DB`. Spuštění test suite **na serveru** proto zapíše fixture texty do reálného `reminder.log` (prod `reminder_fires` zůstává čistá — ta jede přes DB_PATH). **Testy spouštět jen lokálně** (`uv run --with pytest --with croniter pytest skills/remind/tests/`).
|
||
|
||
**Náhodný (deterministický) čas (`random` blok):** N× denně v náhodný čas uvnitř okna, ale deterministicky — sdílený modul `scripts/random_times.py` počítá časy ze seedu `f"{datum}|{text}"`, takže sender zůstává bezstavový (počítá se každou minutu znovu). Min. rozestup mezi časy = konstanta `MIN_GAP_MIN` (default 15) v tomtéž modulu. Validace v `remind_cli.py` jde přes stejný `compute_fire_times`. (`random_times.py` taky drží sdílené `minutes_to_hhmm` — dřív duplikované v cli i send.) Testy: `uv run --with pytest pytest skills/remind/tests/`. Návrh: [plans/remind-random-time.md](plans/remind-random-time.md).
|
||
|
||
**Týdenní náhodný režim (`period='week'`, od 2026-06-15):** `--random-times-per-week N` (mutually exclusive s `--random-times-per-day`) rozprostře N odpálení přes týden 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":"<id>","messages":[{"role":"user","content":"say OK"}],"max_tokens":20}'` (viz sekce o extra usage níže).
|
||
2. **Edituj config in-place** přes Python (zachová ostatní klíče včetně secrets):
|
||
|
||
```bash
|
||
ssh nanobot@nanobot.hell 'python3 -c "
|
||
import json, pathlib
|
||
p = pathlib.Path.home() / \".nanobot/config.json\"
|
||
c = json.loads(p.read_text())
|
||
c[\"modelPresets\"][\"<preset-name>\"] = {\"provider\": \"<ollama|openrouter>\", \"model\": \"<model-id>\"}
|
||
p.write_text(json.dumps(c, indent=2))
|
||
"'
|
||
```
|
||
|
||
3. **Restartuj službu**, aby gateway preset načetla:
|
||
|
||
```bash
|
||
ssh nanobot@nanobot.hell 'XDG_RUNTIME_DIR=/run/user/1000 systemctl --user restart nanobot.service'
|
||
```
|
||
|
||
4. **V chatu** (Telegram/WebUI) přepneš příkazem `/model <preset-name>`.
|
||
|
||
**Konvence pojmenování presetů:** krátký alias podle modelu — `kimi`, `kimi27`, `kimi3`, `glm`, `glm52`, `sonnet`, `haiku`, `gemini-flash`. (Dřív tu stálo `<model>-<provider>` jako `kimi-k2.6-openrouter`; reálný stav na serveru je od nějaké doby krátká forma, ověřeno 2026-07-27.)
|
||
|
||
**Parametry presetu:** `maxTokens` 16384 a `temperature` 0.1 napříč všemi presety. `contextWindowTokens` se drží na **~97 % reálného okna modelu** (rezerva na výstup), reálné okno se čte z `curl http://nvidia.hell:11434/api/show -d '{"model":"<id>"}'` → `model_info["<family>.context_length"]`. `reasoningEffort: null` = zachovat default providera (`schema.py:141`), explicitní hodnota jen kde ji chceme vynutit (`glm52: high`).
|
||
|
||
**Ollama gotcha:** `providers.ollama.apiBase` musí končit `/v1` (`http://nvidia.hell:11434/v1`) — viz [[Ollama provider potřebuje `/v1` suffix v `apiBase`]].
|
||
|
||
## Ollama Cloud: některé modely jsou „extra usage only" (kimi-k3)
|
||
|
||
`kimi-k3:cloud` je v `api/tags` vidět a `/model kimi3` v nanobotu se přepne bez chyby, ale **každé volání skončí HTTP 402**:
|
||
|
||
```text
|
||
this model uses extra usage only (not included plan usage) and your extra usage
|
||
balance is empty, add extra usage or turn on auto reload at ollama.com/settings
|
||
```
|
||
|
||
Nanobot to uživateli přeloží na *„API key is out of quota or the account is in arrears"* — což svádí na chybu v configu; **není to chyba configu**. Ollama má dvě oddělené peněženky: *plan usage* (zahrnuto v Pro/Max) a *extra usage balance* (dokupovaný kredit). K3 je zařazený mimo plán a čerpá **jen** z extra usage — podle [model page](https://ollama.com/library/kimi-k3) vyžaduje Pro/Max **a** nenulový extra balance, kapacita se teprve rozšiřuje. Billing je GPU-time-based, per-token sazba pro extra usage není veřejně zveřejněná.
|
||
|
||
**Diagnostika:** ostatní cloud modely (`glm-5.2:cloud`, `kimi-k2.7-code:cloud`) přitom jedou normálně — když 402 sedí jen na jednom modelu, je to tahle politika, ne stav účtu.
|
||
|
||
**Alternativa bez dobíjení:** stejný model na OpenRouteru jako `moonshotai/kimi-k3` (ctx 1 048 576, $3/M in, $15/M out) — provider `openrouter` je v configu nakonfigurovaný.
|
||
|
||
Zdroj: [Ollama pricing](https://ollama.com/pricing), [ollama.com/library/kimi-k3](https://ollama.com/library/kimi-k3), ověřeno 2026-07-27 (history).
|
||
|
||
## Logování: gateway `-v`/`--verbose`, agent `--logs`
|
||
|
||
Nanobot defaultně **vypíná vlastní logy** (`logger.disable("nanobot")`), proto v běžném výstupu nic není. Zapínají se podle příkazu:
|
||
|
||
- **`nanobot gateway -v` / `--verbose`** → INFO+DEBUG do stderr (u nás přes systemd do journalu). Pokrývá **i WebUI** — WebUI je jen `websocket` channel uvnitř gateway procesu (port 8765), není to samostatná služba, takže žádný separátní přepínač pro WebUI neexistuje.
|
||
- **`nanobot agent --logs` / `--no-logs`** → runtime log přímo v interaktivním CLI chatu. Jiný flag než gateway, nejsou zaměnitelné.
|
||
|
||
Žádná env proměnná ani config klíč pro log level mimo tyhle flagy neexistuje.
|
||
|
||
**Nasazení u nás:** `-v` přidáno do `ExecStart` v `~/.config/systemd/user/nanobot.service` na `nanobot.hell`. Logy živě: `ssh nanobot@nanobot.hell 'journalctl --user -u nanobot.service -f --no-pager'`.
|
||
|
||
**Co `-v` ukáže v jednom tahu** (ověřeno na WebUI zprávě):
|
||
|
||
- `Processing message from <channel>:<id>: <text>` — příchozí zpráva
|
||
- stavy agentního tahu: `RESTORE → COMPACT → COMMAND → BUILD → RUN → SAVE → RESPOND` (každý s časem)
|
||
- `Tool call: <nástroj>({...args...})` — **volání toolu i s argumenty** (INFO)
|
||
- `LLM usage: prompt=… completion=… cached=…` — spotřeba tokenů každé iterace agentní smyčky
|
||
- `Response to <channel>:<id>: <text>` — finální odpověď
|
||
|
||
**Co se NEloguje:** tělo tool výsledku (stdout), plné LLM zprávy ani thinking. Thinking jde samostatným kanálem do klienta (WebUI), ne do journalu. `-v` je serverová záležitost — ve WebUI se nic nezmění.
|
||
|
||
**Pozor:** `-v` zapíná INFO+DEBUG globálně, takže v journalu jsou i heartbeat/cron/dream tahy.
|
||
|
||
Startup taky vypíše užitečné: `Registered N tools: [...]` (výčet dostupných toolů) a `Runtime model switched … <model>` (aktivní preset).
|
||
|
||
## Srovnání modelů pro nanobot (cloud inference)
|
||
|
||
Hodnoceno pro mix: agentní úlohy (tool use, Dream, skilly) + rychlost + Python. Platí pro cloud Ollama i OpenRouter — hardwarové podmínky jsou srovnatelné. **Provider-agnostic pohled** (předpokládá dostupnost rychlé inference).
|
||
|
||
> **Za podmínky Ollama Cloud (žádný rychlý provider) to upřesňuje [`models.md`](models.md)** — tam rozhoduje latence, takže pro interaktivní vrstvu vede **GLM-5.1**, ne Kimi. Tahle tabulka a `models.md` se nerozcházejí v datech, jen v východisku: provider-agnostic vs. fixní Ollama Cloud.
|
||
|
||
| Pořadí | Model | Proč |
|
||
|--------|-------|------|
|
||
| 1 | **Kimi K2** (`kimi-k2.6-*`) | Jediný explicitně trénovaný na agentní úlohy a tool use; MoE ~32B aktivních params = rychlý |
|
||
| 2 | **Qwen 3.6+** (`qwen-3.6-plus-openrouter`) | Pravděpodobně Qwen3 235B-A22B (~22B aktivních = nejrychlejší v seznamu); top coding, silné instruction following |
|
||
| 3 | **DeepSeek V3.2** (`deepseek-v3.2-ollama`) | Nejlepší Python, nejsilnější instruction following; ~37B aktivních; ideální pro Dream |
|
||
| 4 | **Qwen 3.5** (`qwen3.5-ollama`) | Solidní záloha, dobrý coding, rychlý |
|
||
| 5 | **GLM-5.1** (`glm-5.1-ollama`) | Dobrý model, ale za Kimi/Qwen/DeepSeek na všech osách |
|
||
| 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/<name>/` se souborem `SKILL.md` (YAML frontmatter s `name` + `description`, tělo markdown instrukce). Bootstrap soubory se čtou při každém tahu bez restartu. Žádný `install` příkaz neexistuje — skill se vytvoří ručně (nebo ho Dream vytvoří sám).
|
||
|
||
**Clawhub.ai / OpenClaw** je jiný ekosystém, nemá s nanobotem nic společného. Skilly odtud je třeba manuálně adaptovat.
|
||
|
||
**Claude Code skilly jsou přímo přenositelné.** Anthropic Skills format (`SKILL.md` s YAML frontmatter `name`+`description` + markdown tělo) je identický s nanobot skill formátem. Stačí zkopírovat složku `skills/<name>/` ze zdroje (např. plugin `.claude-plugin/skills/<name>/`) do `~/.nanobot/workspace/skills/<name>/` — žádná konverze. **Manifest `.claude-plugin/plugin.json` se neinstaluje**, je Claude-Code-specific. Pozor jen na (a) reference na Claude-Code tooly v těle skillu (`TodoWrite`, `ExitPlanMode`, `AskUserQuestion` apod. v nanobotovi neexistují), (b) prompt-injection v markdown těle — nanobot čte skill jako součást system contextu. Ověřeno: nasazen `grill-me` z [mattpocock pluginu](https://github.com/lachtan/nicecode/tree/master/plugins/mattpocock) (history 2026-05-28 "Pilot mattpocock skillu grill-me").
|
||
|
||
Zdroj: `nanobot/agent/skills/`, `ContextBuilder._load_bootstrap_files()`
|
||
|
||
## Skill `description` — k čemu reálně slouží (progressive loading)
|
||
|
||
Pole `description` ve frontmatteru non-always skillu je **routing signál**, ne kontext „jak skill funguje". Při sestavování system promptu se každý non-always skill vykreslí jako **jeden řádek** v seznamu: `- **<name>** — <description> \`cesta/k/SKILL.md\``. Tělo SKILL.md se načte **až on-demand**, když si agent skill sám přečte přes`read_file`. Důsledky:
|
||
|
||
- `description` je jediná info o skillu v promptu, dokud agent nečte tělo → patří tam jen *kdy/proč* skill spustit (trigger fráze, odlišení od příbuzných skillů), **ne** *jak* funguje.
|
||
- `description` se **nezkracuje** (`_get_skill_description` vrací text doslova) a je v promptu **každý tah** u všech skillů → trvalý token cost. Drž stručně, routing-orientovaně. Detailní postup patří do těla.
|
||
- **Always skilly** (`metadata.nanobot.always: true`): `description` se **ignoruje úplně**, do promptu se eager vkládá **celé tělo** (bez frontmatteru). Druhý vysvětlující odstavec v `description` je u nich čistý šum.
|
||
|
||
**Co tedy patří do `description`:** jen *kdy/proč* skill spustit — krátká věta o účelu + trigger fráze + případné odlišení od příbuzného skillu. **Nepatří** tam *jak* skill funguje (to do těla, čte se on-demand) ani detailní postup. Triggery nemusí být dvojjazyčné — model rozpozná záměr napříč jazyky, takže explicitní CZ varianty nic nepřidají, jen prodlužují řádek (ověřeno na `/plan`, 2026-05-31).
|
||
|
||
Zdroj: `nanobot/agent/skills.py:111-159` (`build_skills_summary`, `_get_skill_description`), `skills.py:94-109` (`load_skills_for_context`, always skilly), `nanobot/agent/context.py:87-95`.
|
||
|
||
## Dream procesor — automatické self-improvement
|
||
|
||
Nanobot má vestavěný Dream procesor (`agent/memory.py:Dream`) který běží každé 2 hodiny. Jde o **dvou-fázový LLM pipeline** nad `history.jsonl`:
|
||
|
||
- **Fáze 1:** Plain LLM call analyzuje historii, hledá fakta (`[MEMORY]`/`[USER]`/`[SOUL]`), kandidáty na smazání (`[FILE-REMOVE]`), opakující se workflow (`[SKILL]`)
|
||
- **Fáze 2:** AgentRunner s `read_file`/`edit_file`/`write_file` tools provede chirurgické editace; umí sám vytvářet nové skilly (`skills/<name>/SKILL.md`)
|
||
|
||
Dream řeší: deuplikaci, detekci stale obsahu (git blame age na řádcích MEMORY.md), automatické git commity po změnách. Cursor v `.dream_cursor` zabraňuje přepracování.
|
||
|
||
**Důsledek:** Self-improving-agent skilly z jiných ekosystémů jsou z velké části redundantní — Dream pokrývá jejich core funkcionalitu nativně. Přidaná hodnota by byl jen okamžitý strukturovaný error log (ERR-YYYYMMDD-XXX formát) — Dream čeká 2h.
|
||
|
||
Zdroj: `nanobot/agent/memory.py:Dream`, prompt templates `agent/dream_phase1.md`, `agent/dream_phase2.md`
|
||
|
||
---
|
||
|
||
## Non-interactive nanobot CLI: streamuje chaoticky, Python API vrací čistý string
|
||
|
||
`nanobot agent --message "..." --session "..."` projede agent loop, výstup
|
||
ale **streamuje rozkouskovaně přes stdout** (`✻` prefixované delty
|
||
reasoning/progress, finální `response.content` až na úplném konci). I při
|
||
`--no-markdown` a pipe (`| cat`) jde streaming dál. Postprocesovat by bylo
|
||
křehké.
|
||
|
||
**Pro programatické použití** (daemon, skript) jdi přes Python API:
|
||
|
||
```python
|
||
import asyncio
|
||
from nanobot import Nanobot
|
||
bot = Nanobot.from_config()
|
||
result = await bot.run("prompt", session_key="my:session")
|
||
# result.content je čistý string, žádné streamovací nečistoty
|
||
```
|
||
|
||
Interpreter s `import nanobot`: `/home/nanobot/.local/share/uv/tools/nanobot-ai/bin/python`.
|
||
Loguru jde na stderr (lze odchytit nebo přesměrovat). `Nanobot.run` interně volá
|
||
`AgentLoop.process_direct` **bez cron preamble** — to je jen v `on_cron_job` callbacku.
|
||
|
||
Zdroj: `nanobot/cli/commands.py:1204-1231` (CLI), `nanobot/nanobot.py:71-102` (`Nanobot.run`).
|
||
|
||
---
|
||
|
||
## Agent vidí `Channel` a `Chat ID` v runtime contextu zprávy
|
||
|
||
ContextBuilder každý tah příchozí zprávy obaluje runtime context blokem,
|
||
ve kterém je `Channel: <name>` a `Chat ID: <id>` (kromě `Current Time` a
|
||
volitelně `Sender ID`). Skill nebo prompt si je tedy **může přečíst** —
|
||
nemusí mít vlastní tool ani contextvars přístup.
|
||
|
||
```
|
||
Channel: telegram
|
||
Chat ID: 8826147089
|
||
```
|
||
|
||
V CLI / SDK session bez channel kontextu se blok nezobrazí (`Chat ID`
|
||
chybí). Skill na to musí umět reagovat (např. `detach` v takovém
|
||
případě nabídne synchronní vykonání).
|
||
|
||
Zdroj: `nanobot/agent/context.py:123-139` (`ContextBuilder._build_runtime_context`).
|
||
|
||
---
|
||
|
||
## Cron preamble je hardcoded — pro non-reminder background úlohy obejít
|
||
|
||
`nanobot/cli/commands.py:891-897` (`on_cron_job`) obaluje payload natvrdo:
|
||
|
||
```
|
||
The scheduled time has arrived. Deliver this reminder to the user now,
|
||
as a brief and natural message in their language. Speak directly to them —
|
||
do not narrate progress, summarize, include user IDs, or add status reports
|
||
like 'Done' or 'Reminded'.
|
||
|
||
Reminder: <payload.message>
|
||
```
|
||
|
||
Pro reminders je to správné chování. Pro background **úlohy** (deep research,
|
||
ingest, multi-step research) je to v přímém rozporu — agent má provést úkol,
|
||
zapsat výsledek do souboru, vrátit informativní větu. Preamble ho stáhne
|
||
do meta-statusu.
|
||
|
||
**Cesta okolo:** zahodit cron tool i `at` jednorázové joby, orchestraci řešit
|
||
**externím daemonem mimo agent loop** — viz "Detach skill" níže. Stejný pattern
|
||
už používá `/remind` (viz "Cron job s LLM agentem je nespolehlivý…" výše).
|
||
|
||
**AKTUALIZACE (ověřeno 2026-07-15 na `nanobot_ai-0.2.2` živém na serveru): tohle popisuje jen fallback pro "unbound" joby.** Skutečná `agent_turn` architektura je jiná — viz "Cron `agent_turn` job = normal session turn (bound cron, od `nanobot_ai` 0.2.2)" níže. `on_cron_job` v `cli/commands.py` má teď větev `is_bound_cron_job(job)` → `run_bound_cron_job(...)`; jen když job **není** bound (chybí `session_key`/`origin_channel`/`origin_chat_id`, nebo má vyplněné staré `deliver`/`channel`/`to`/`channel_meta`), spadne do staré větve výše s hardcoded preamblem — a ta dnes končí `CronJobSkippedError("unbound agent cron job must be recreated from a chat session")`, ne úspěšným během. Reálný job v `jobs.json` (`nanobot-version-check`) je bound.
|
||
|
||
---
|
||
|
||
## Cron `agent_turn` job = normal session turn (bound cron, od `nanobot_ai` 0.2.2)
|
||
|
||
**Kontext, který LLM dostane při odpálení cron jobu, je (skoro) identický s běžnou zprávou v té session, kde byl job vytvořen** — ne izolovaný/ephemerní jednorázový volání jako dřív (`process_direct`). Ověřeno čtením zdroje na serveru (`nanobot_ai-0.2.2.dist-info`), potvrzeno reálným `jobs.json` (job `nanobot-version-check`: `sessionKey: "websocket:52d0f338-…"`, `originChannel: "websocket"`, `originChatId: "…"`, `deliver: false`, `channel: null`).
|
||
|
||
**Tok (`nanobot/cron/bound_runner.py:run_bound_cron_job`):**
|
||
|
||
1. Job musí být **bound** (`nanobot/cron/session_turns.py:is_bound_cron_job`): `payload.kind == "agent_turn"` + `session_key`/`origin_channel`/`origin_chat_id` vyplněné + staré delivery pole (`deliver`/`channel`/`to`/`channel_meta`) prázdné. Bound joby vznikají tak, že se vytvoří z chatu (agent zná runtime `Channel`/`Chat ID` z kontextu a předá je do `cron` toolu).
|
||
2. Zpráva se vyrenderuje z šablony `templates/agent/cron_reminder.md`: *"The scheduled time has arrived. Execute this scheduled cron job now and report the result to the user in the same session."* + pravidla (mluv přímo, nenarrativizuj, žádné user ID, žádné "Done") + `Cron job: {{ message }}` (= `payload.message`).
|
||
3. Sestaví se `InboundMessage` s `channel`/`chat_id` = **origin** hodnoty z jobu, `session_key_override = payload.session_key`, a metadata `_cron_trigger` (job_id, job_name, run_id, prompt_ref) + `_cron_defer_until_session_idle: true`.
|
||
4. Ta zpráva jde přes `agent.submit_cron_turn()` → `CronTurnCoordinator.submit()` → **stejnou frontu `bus.consume_inbound()`/`_dispatch()`/`_process_message()` jako kterákoli chatová zpráva** (`agent/loop.py:run`/`_dispatch`). Žádná zkratka, žádný oddělený "cron mód" LLM volání.
|
||
|
||
**Co z toho plyne pro obsah promptu, který LLM vidí:**
|
||
|
||
- **Celý systémový prompt jako obvykle** — bootstrap soubory (`AGENTS.md`/`SOUL.md`/`USER.md`/`TOOLS.md`), `memory/MEMORY.md`, always-skilly (`ContextBuilder.build_messages`, stejná cesta jako běžný tah).
|
||
- **Celá historie té session** (`session_key` = session, ve které byl cron job vytvořen — typicky WebUI/Telegram konverzace), ne prázdná/ephemerní session. Cron zpráva se do historie i zapíše (`_persist_user_message_early` s `cron_history_overrides`), ale zobrazená verze v historii je zkrácená: `"Scheduled cron job triggered: <job_name>\n\n<payload.message>"`, ne celá vyrenderovaná šablona — a metadata na ní má `cron_job_id`/`cron_job_name`/`cron_run_id`/`cron_prompt_ref`.
|
||
- **Runtime context blok** (`Channel`/`Chat ID`/`Current Time`) ukazuje origin channel/chat_id jobu — model tedy vidí, do jakého kanálu odpovídá, stejně jako u normální zprávy.
|
||
- **Všechny toolly** jsou k dispozici stejně jako v normálním tahu (žádný omezený tool subset jako u Dream `build_dream_tools()`).
|
||
- **Odpověď se doručí automaticky** — finální text tahu (`_process_message` return) jde přes `bus.publish_outbound()` do origin channel/chat_id, přesně jako běžná odpověď. Žádný druhý fail-open `evaluate_response` gate (ten zůstal jen u `heartbeat` větve).
|
||
|
||
**Deferral:** pokud je cílová session zrovna aktivní (probíhá jiný tah), `CronTurnCoordinator.defer_if_active()` job zafrontuje a spustí ho, až se session uvolní (`publish_next_deferred` po dokončení běžícího tahu) — cron turn tedy nikdy neběží souběžně s live chatem ve stejné session, ale ani ho nezahodí.
|
||
|
||
**Praktický důsledek:** cron `agent_turn` job dnes **umí** to, co starý hardcoded preamble bránil (viz sekce výše) — protože jede jako normální tah, může provést multi-step úkol, zapsat soubor, a odpověď je jen finální zpráva tahu, ne meta-status obalený cizím promptem. `nanobot-version-check` job v produkčním `jobs.json` toho využívá (kontrola PyPI verze + podmíněné oznámení).
|
||
|
||
Zdroj: `nanobot/cron/bound_runner.py`, `nanobot/cron/session_turns.py`, `nanobot/agent/cron_turns.py` (`CronTurnCoordinator`), `nanobot/agent/loop.py:574-578,868-870,1035-1117` (`submit_cron_turn`, `_dispatch`), `nanobot/templates/agent/cron_reminder.md`, `nanobot_ai-0.2.2` na `nanobot.hell` (živý zdroj, ne upstream repo).
|
||
|
||
---
|
||
|
||
## Bound cron job sdílí session s chatem — důsledky pro růst kontextu, `/new` a (ne)možnost odpoutání
|
||
|
||
Navazuje na sekci výše. Bound cron job **nemá vlastní session** — `payload.sessionKey` je přesně ten samý klíč jako chat, odkud vznikl (`CronTool.set_context()`: `session_key = f"{channel}:{chat_id}"`). Každé odpálení jobu appendne celý svůj tah (trigger zpráva + tool volání + finální odpověď) do `session.messages` té konverzace — soutěží o prostor s reálným chatem, ne v izolaci.
|
||
|
||
**Co to omezuje a co ne:**
|
||
|
||
- **`maxMessages: 120`** (server `config.json`, `agents.defaults.maxMessages` / schema default 120) — do LLM promptu se replayuje jen posledních 120 zpráv (`session/manager.py::get_history`). Token náklad per-tah tedy neroste do nekonečna, stará historie se jen vysouvá z okna.
|
||
- **`idleCompactAfterMinutes: 0` na `nanobot.hell` — auto-compact (`AutoCompact`) je VYPNUTÝ.** Default v schématu je 15 min; tady je natvrdo 0, takže `check_expired()` (`agent/loop.py`, volané na 1s timeoutu hlavní smyčky) nikdy nic nekomprimuje na 8 zpráv + summary.
|
||
- I kdyby zapnutý byl: **periodický cron job v té session drží `session.updated_at` čerstvý** → session nikdy nevypadá jako idle → auto-compact by se pro ni stejně nikdy nespustil. Recurring cron bound na chat tedy fakticky blokuje auto-compact té session, i kdyby byl jinde v config zapnutý.
|
||
- Výsledek: `session.messages` na disku roste bez omezení (jen replay do LLM je capnutý na 120) a nic z toho neprochází konsolidací do `MEMORY.md`/Dream, dokud uživatel neudělá `/new`.
|
||
|
||
**`/new` (`cmd_new`, `command/builtin.py:205`) čistí přesně tenhle sdílený session_key** — `session.clear()` + na pozadí archivace starého obsahu přes `consolidator` (feed pro Dream). Protože cron job běží ve stejné session, `/new` v daném chatu **vyprázdní i kontext, který cron job uvidí** při příštím odpálení (stará historie se archivuje, ne ztratí — jen zmizí z "recent" replaye).
|
||
|
||
**Odpoutat cron job od session přes `cron` tool nejde — strukturálně.** `CronTool._add_job` (`agent/tools/cron.py`) bere `session_key`/`origin_channel`/`origin_chat_id` vždy z aktuálního request kontextu; bez nich vrátí chybu `"scheduled cron jobs must be created from a chat session"`. Parametr `deliver` v `execute()` existuje, ale nikam se nepředává do `add_job` — mrtvý pozůstatek staré (unbound) architektury. Ruční editace `jobs.json` na "unbound" tvar (bez `sessionKey`/`originChannel`/`originChatId`) taky nepomůže — `is_bound_cron_job()` vrátí `False` a `on_cron_job` rovnou shodí `CronJobSkippedError("unbound agent cron job must be recreated from a chat session")`; **stará větev s hardcoded preamblem (viz sekce "Cron preamble je hardcoded" výše) se pro `agent_turn` joby dnes nikdy neprovede — je to mrtvá cesta.**
|
||
|
||
**Pro skutečně nezávislou (session-decoupled) periodickou úlohu použij [[Detach skill]] níže** — vlastní `session_key` namespace (`detach:<slug>`), mimo cron/jobs.json úplně, doručení přímo přes Telegram Bot API bez účasti chatové session.
|
||
|
||
Zdroj: `nanobot/agent/tools/cron.py` (`CronTool._add_job`, `set_context`), `nanobot/agent/autocompact.py` (`AutoCompact`), `nanobot/config/schema.py:147-156` (`session_ttl_minutes`/`idleCompactAfterMinutes`, `max_messages`/`maxMessages`), `nanobot/session/manager.py` (`get_history` replay slicing), `nanobot/command/builtin.py:205` (`cmd_new`), živý `~/.nanobot/config.json` na `nanobot.hell` (`idleCompactAfterMinutes: 0`, `maxMessages: 120`) — ověřeno 2026-07-15.
|
||
|
||
### Upstream stav: žádný flag na vypnutí sdílení session zatím neexistuje (ověřeno 2026-07-15)
|
||
|
||
Sdílení session cronu s originálním chatem (viz výše) je **záměrný design**, ne bug: [PR #4299](https://github.com/HKUDS/nanobot/pull/4299) *"feat(cron): bind scheduled automations to sessions"* (mergnuto 2026-06-12) ho zavedlo cíleně, aby se vyřešilo doručování/injektování zpráv doprostřed živého chatu (`bound_runner.py` vznikl přesně touhle PR).
|
||
|
||
Na neomezený růst kontextu si ale stěžují i jinde a řeší se to teď (oba stavy **open, nemergnuto**):
|
||
|
||
- [Issue #4082](https://github.com/HKUDS/nanobot/issues/4082) — *"cron jobs reuse fixed cron:{job.id} session context across runs"*, navrhuje buď per-run isolaci, nebo *"an explicit config flag controlling cron context retention"*.
|
||
- [PR #4550](https://github.com/HKUDS/nanobot/pull/4550) — řeší to per-run izolovanou session (`session_key:run_id`, smazanou hned po tahu přes nový `agent.delete_session()`), **bez** configurovatelného flagu. Autor PR k automatizované review připomínce (ztráta cross-run kontinuity pro monitoring/diff joby) napsal: *"a future `shared_session` option could be added, but that's a design decision for maintainers"* — tedy jen nápad, ne odsouhlasený roadmap item, žádný maintainer zatím nezareagoval. `mergeable_state: dirty` (2026-07-06).
|
||
- Starší konkurenční [PR #2457](https://github.com/HKUDS/nanobot/pull/2457) (open od 2026-03-25) řeší podobný problém na starším kódu, pravděpodobně obsoletní vůči #4550.
|
||
|
||
**Souvisí:** [PR #4370](https://github.com/HKUDS/nanobot/pull/4370) *"Enable idle auto-compact by default"* (mergnuto 2026-06-16) změnilo schema default `idleCompactAfterMinutes` z `0` na `15` — právě kvůli krátkým chatům, co se nikdy nekompaktují. Náš server má v `config.json` **explicitní `0`** (přebíjí schema default bez ohledu na verzi) — nezávisí na tom, jestli byl nastaven před nebo po téhle změně.
|
||
|
||
**Akce:** sledovat #4082/#4550 (a případně #2457), jestli se mergne a jestli přibude `shared_session` flag — pak přehodnotit, jestli přenastavit `nanobot-version-check` (a budoucí podobné joby) na isolated mód. Zatím jediná cesta k plnému odpoutání = [[Detach skill]] níže.
|
||
|
||
---
|
||
|
||
## Detach skill — background úlohy přes externí daemon (mimo agent loop)
|
||
|
||
Architektura podobná `/remind` — orchestrace mimo agent loop, žádný cron preamble.
|
||
|
||
**Tok:**
|
||
|
||
1. Skill `detach` v chatu → `exec skills/detach/scripts/create-task.py --goal … --slug … --channel … --chat-id …`. Skript vygeneruje timestamp + frontmatter, zajistí fronty (`mkdir -p`), atomicky zapíše do `tasks/tmp/` a přesune do `tasks/inbox/` (atomický rename, partial-write race neexistuje). Agent dělá jen LLM části (přeformulovat goal, vybrat slug, přečíst channel/chat_id z runtime contextu) — žádný ruční `write_file`/`mv`/`date`.
|
||
2. Systemd user unit `tasks-daemon.path` (`DirectoryNotEmpty=…/tasks/inbox`) přes inotify spustí `tasks-daemon.service` (`Type=oneshot`).
|
||
3. `tasks-daemon.py` (Python, shebang na uv venv interpreter) projede inbox: `mv → running/`, parse frontmatter (`chat_id` povinný), zavolá `Nanobot.from_config().run(goal, session_key=f"detach:<stem>")` s 45-min timeoutem, appendne `## Result` sekci, `mv → done/` nebo `failed/`, pošle Telegram zprávu přes Bot API (urllib + token z `~/.nanobot/config.json["channels"]["telegram"]["token"]`).
|
||
|
||
**Volba modelu pro task (od 2026-06-07):** Detach umí task spustit na explicitně zvoleném presetu (background = latence nebolí, vyplatí se silnější model). Uživatel model jen zmíní ve větě („na kimi") → agent předá token jako `create-task.py --model "<token>"` → skript ho **při captue** fuzzy-resolvne proti `config.json` (`resolve_preset`: exact case-insensitive → unikátní substring; jinak `KeyError` se seznamem, exit 1, fail-fast v chatu) a uloží přesný preset do frontmatteru `model:`. Bez `--model` jede default (`agents.defaults.modelPreset`). Daemon přečte `fm["model"]` a před `run()` přepne `bot._loop.set_model_preset(preset)` — stejný switch jako `/model` v chatu (ověřeno e2e s nainstalovaným balíčkem, history 2026-06-07). **Gotcha:** klíč presetů je v serverovém `config.json` na disku **snake_case `model_presets`** (ne camelCase `modelPresets`), zatímco `agents.defaults.modelPreset` je camelCase — `load_preset_names()` proto čte oba tvary.
|
||
|
||
**Soubory:**
|
||
|
||
- `~/.nanobot/workspace/skills/detach/SKILL.md` — definice + triggery (EN-only)
|
||
- `~/.nanobot/workspace/skills/detach/scripts/tasks_common.py` — sdílené čisté helpery (TASKS, FILENAME_RE, parse_frontmatter, parse_kv, format_*, build_task_*), importují ho ostatní skripty
|
||
- `~/.nanobot/workspace/skills/detach/scripts/create-task.py` — capture skript (frontmatter + atomický tmp→inbox)
|
||
- `~/.nanobot/workspace/skills/detach/scripts/{list-tasks,read-task}.py` — list / read subactions
|
||
- `~/.nanobot/workspace/skills/detach/tests/` — pytest čisté logiky (lokálně v repu, ne na serveru)
|
||
- `~/.nanobot/workspace/skills/detach/scripts/tasks-daemon.py` — daemon
|
||
- `~/.nanobot/workspace/skills/detach/systemd/tasks-daemon.{path,service}` — user systemd unity (symlinkované do `~/.config/systemd/user/`)
|
||
- `~/.nanobot/workspace/tasks/{tmp,inbox,running,done,failed}/` — fronty
|
||
- `~/.nanobot/workspace/log/tasks-daemon.{log,stdout.log,stderr.log}` — append-only logy
|
||
|
||
**Souběh:** systemd serializuje (`Type=oneshot` se nespustí podruhé, dokud první běh trvá; level-triggered `.path` ho restartne po doběhu pokud inbox stále není prázdný). Žádný flock není potřeba.
|
||
|
||
**Notifikační target — Telegram s fallback chat_id (single-user setup):** Skill v frontmatteru zapíše `channel` + `chat_id` z runtime contextu (`Channel: telegram` → numeric ID, `Channel: websocket` → session UUID, atd.). Daemon `resolve_telegram_chat_id(fm)`:
|
||
|
||
- pokud `channel == "telegram"` → použij `chat_id` z frontmatteru (multi-user ready)
|
||
- jinak → čti `channels.telegram.allowFrom[0]` z `~/.nanobot/config.json`
|
||
|
||
Tím Telegram vždy doručí, i když úkol přišel z WebUI / CLI. Daemon log: `NOTIFY chat=<id> source=<frontmatter|fallback>`. Bez tohoto fallbacku selhával Telegram Bot API s HTTP 400 pro non-telegram channel (history 2026-05-28 18:37).
|
||
|
||
**Subactions `list` a `read`:** detach skill umí i číst zpět hotové úkoly. „výsledky?" → markdown tabulka tasks/{running,done,failed}/. „výsledek <slug-nebo-pattern>" → `read_file` přes match v done/+failed/, předlož `# Result` sekci. Identifier match: slug substring (`*foo*`), timestamp fragment (`*T175451*`), nebo prázdný = nejnovější.
|
||
|
||
**Zdroj:** [skills/detach/](skills/detach/) v tracking repu, history 2026-05-28 „Skill detach + daemon" + iterace #2 + iterace #3.
|
||
|
||
**uv-native invokace (iterace #3):** Shebang přepnut na `#!/usr/bin/env -S uv run --script` s PEP 723 inline metadata (`requires-python = ">=3.11"`, `dependencies = ["nanobot-ai"]`). `uv run --script` samo vytvoří/cachuje izolované venv — skript přežije `uv tool uninstall/install` i přesun na jiný stroj. První spuštění po PEP 723 změně trvá ~5-10s (budování venv), další jsou instantní (cache v `~/.cache/uv/`). Systemd user unit musí mít `Environment=PATH=%h/.local/bin:/usr/bin:/bin`, jinak `uv` v PATH chybí.
|
||
|
||
---
|
||
|
||
## Detach notifikace do origin kanálu (WebUI/CLI) — záměrně nepodporováno
|
||
|
||
Daemon notifikuje **jen Telegram** (přes Bot API, deterministicky). Když task přišel z WebUI nebo CLI, do toho kanálu se notifikace nepošle — uživatel si výsledek vyzvedne přes `výsledek <slug>` (detach subaction `read`).
|
||
|
||
**Architektonický důvod:** WebSocket spojení vlastní gateway proces; daemon je samostatný systemd oneshot. Nanobot nemá HTTP endpoint pro vstřikování zpráv do WS sessions (`nanobot/channels/websocket.py:673-782` — všechny `/api/sessions/...` jsou read-only). Sdílí jen filesystem, žádné IPC.
|
||
|
||
**Zvážené a zamítnuté možnosti:**
|
||
|
||
- **Samostatný `Nanobot.run()` jen kvůli notifikaci** — LLM jako IPC proxy. Pomalé (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 <slug>` je dokumentovaný způsob pro WebUI/CLI.
|
||
|
||
---
|
||
|
||
## Skill `exec` běží z workspace rootu, ne ze skill adresáře
|
||
|
||
Když skill volá `exec` bez explicitního `working_dir`, příkaz běží s **CWD = workspace root** (`~/.nanobot/workspace`), **ne** v adresáři skillu. Cesty na skripty skillu proto musí být buď workspace-relativní (`skills/<name>/scripts/x.py`) nebo absolutní — **skill-dir-relativní `scripts/x.py` se rozbije** (resolvuje na `workspace/scripts/x.py`).
|
||
|
||
Zdroj: upstream `nanobot/agent/tools/shell.py:148` (`working_dir=ctx.workspace`) + `:370` (`cwd = working_dir or workspace_root`). Pozn.: remind SKILL.md používá workspace-relativní `skills/remind/scripts/remind_cli.py` (korektní); detach analogicky `skills/detach/scripts/…`.
|
||
|
||
---
|
||
|
||
## Python skripty na serveru — uv-native pattern (PEP 723)
|
||
|
||
Preferovaný způsob pro libovolný stand-alone Python skript v `~/.nanobot/workspace/`:
|
||
|
||
```python
|
||
#!/usr/bin/env -S uv run --script
|
||
# /// script
|
||
# requires-python = ">=3.11"
|
||
# dependencies = ["nanobot-ai", "requests", ...]
|
||
# ///
|
||
```
|
||
|
||
`uv run --script` vytvoří a cachuje izolované venv per skript (`~/.cache/uv/`). Skript přežije `uv tool uninstall/install`, upgrade Pythonu i přesun stroje — bez přímé cesty do `~/.local/share/uv/tools/<tool>/bin/python`. První spuštění po vytvoření hlavičky trvá ~5-10s (build venv), další jsou instantní.
|
||
|
||
**Gotcha pro user systemd:** unit musí mít explicitní PATH, jinak shebang `uv` nenajde:
|
||
|
||
```ini
|
||
[Service]
|
||
Environment=PATH=%h/.local/bin:/usr/bin:/bin
|
||
ExecStart=%h/path/to/script.py
|
||
```
|
||
|
||
Bez `Environment=PATH` selže s `/usr/bin/env: 'uv': No such file or directory`. Aplikace pravidla na všechny budoucí user systemd unity spouštějící uv skripty (nejen detach).
|
||
|
||
Zdroj: [PEP 723](https://peps.python.org/pep-0723/), [uv docs `uv run --script`](https://docs.astral.sh/uv/guides/scripts/), ověřeno deployem detach skillu iterace #3.
|
||
|
||
---
|
||
|
||
## Systemd `.path` unit s `DirectoryNotEmpty=` — event-driven workspace daemon
|
||
|
||
Pattern pro libovolný daemon, který má reagovat na soubory v workspace **bez polling**:
|
||
|
||
```ini
|
||
# tasks-daemon.path
|
||
[Path]
|
||
DirectoryNotEmpty=%h/.nanobot/workspace/tasks/inbox
|
||
Unit=tasks-daemon.service
|
||
|
||
[Install]
|
||
WantedBy=paths.target
|
||
```
|
||
|
||
```ini
|
||
# tasks-daemon.service
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=%h/path/to/daemon.py
|
||
```
|
||
|
||
`%h` = user home. `.path` unit je jen watcher (přes inotify), reálnou akci dělá `.service`. **Level-triggered:** dokud kondice `DirectoryNotEmpty=` platí, systemd po každém doběhnutí service spustí novou instanci. Daemon by měl drenovat celý inbox v jednom běhu (sériově).
|
||
|
||
Install: `systemctl --user enable --now <unit>.path`. Lingering musí být zapnutý (`loginctl enable-linger nanobot`), jinak user units po odhlášení padnou. Pro reminders se to nepoužívá — ty mají cron výrazy, .path není vhodný (kondice se nemění minutu po minutě). Pro file-driven queue (jako detach) ano.
|
||
|
||
**Gotcha — level-triggered `.path` + startup crash = permanentní latch:** Když oneshot daemon spadne **ve startup fázi** (před vyprázdněním inboxu), inbox zůstane neprázdný → `.path` ho hned znovu spustí → další pád → … Na manager defaultu (`StartLimitIntervalSec=10s`, `Burst=5`) to za <2 s narazí na rate-limit a systemd zalatchuje **`.service` i `.path`** do `failed (unit-start-limit-hit)`. Z toho se **sám nezotaví** — nutný `systemctl --user reset-failed <unit>.service <unit>.path` + `restart <unit>.path`. (Stalo se 7.6., když daemon padal na `NameError`.)
|
||
|
||
**Hardening (ověřeno, nasazeno na tasks-daemon):** v `.service` přidat
|
||
|
||
```ini
|
||
[Unit]
|
||
StartLimitIntervalSec=1800
|
||
StartLimitBurst=20
|
||
[Service]
|
||
Restart=on-failure
|
||
RestartSec=60
|
||
```
|
||
|
||
`Restart=on-failure` + `RestartSec` dá **delay mezi pokusy** (nezávisle na `.path` retriggeru); čistý `exit 0` (inbox vyprázdněn) ani SIGTERM od systemd nerestartují. Širší okno (`30min`/`20`) zajistí, že se latch po posunu okna sám pustí dál. **`man systemd.service`: pro `Type=oneshot` jsou zakázané jen `Restart=always`/`on-success`, `on-failure` je povolený.**
|
||
|
||
Zdroj: `man systemd.path` + `man systemd.service`, ověřeno smoke testem před deployem detach skillu; latch+hardening history 2026-06-07 19:33.
|
||
|
||
---
|
||
|
||
## `nvm` je shell funkce, ne binárka
|
||
|
||
`nvm` je definován jako bash funkce v `.bashrc` — **není to spustitelný soubor**. Proto ho systemd service nevidí, ani když má správně nastavenou `PATH` s nvm node cestou.
|
||
|
||
| Příkaz | Typ | Dostupný v systemd service? |
|
||
|---|---|---|
|
||
| `node`, `npm`, `npx` | skutečné binárky v `.nvm/.../bin/` | ano, pokud je PATH nastavena explicitně |
|
||
| `nvm` | shell funkce v `.bashrc` | **ne nikdy** — `.bashrc` se nesourcuje |
|
||
|
||
Pro správu verzí Node.js z shellu → přihlásit se jako `nanobot` a volat `nvm` interaktivně. Z agenta nebo daemonu → volat `node`/`npx` přímo (fungují přes PATH).
|
||
|
||
---
|
||
|
||
## Skill `/keep` — explicit immediate memory
|
||
|
||
On-demand skill pro okamžitou explicitní paměť. Uživatel řekne „keep X" → agent reformuluje na terse fact → zapíše jako bullet do `workspace/keep.md`. Bez datumů. Dedup, compaction při >150 řádcích.
|
||
|
||
**Persistent awareness:** `keep.md` není v `BOOTSTRAP_FILES` (ty jsou hardcoded). Trvalé povědomí zajišťuje krátká reference `## workspace/keep.md` na konci `USER.md` (auto-loadovaný každý tah). Skill je tedy čistě write endpoint — neplýtvá context window každou session.
|
||
|
||
**Kde žije:** `workspace/keep.md` v rootu workspace (vedle `USER.md`, `MEMORY.md`). Edituje ho výhradně `/keep` skill; ostatní agent paths smí číst. **Odděleno od Dream / MEMORY.md** — Dream o `keep.md` neví, needituje ho.
|
||
|
||
**Dedup pokrývá `keep.md` i `MEMORY.md`:** Write protocol (krok 4) před appendem přečte `workspace/memory/MEMORY.md` a pokud tam je sémanticky podobný fakt (Dream ho mohl destilovat), upozorní uživatele a defaultně přeskočí. `MEMORY.md` je read-only — `/keep` do něj nikdy nezapisuje.
|
||
|
||
**Ukládá i *why*, ne jen *what* (od 2026-06-06):** Krok 2 Write protokolu rozlišuje typ záznamu — plain fakt (alergie, deploy window, jméno) jde bez důvodu; **rozhodnutí / preference / dead-end** dostane důvod inline na stejném řádku (`<fakt> — because <terse why>`). Pokud je vstup rozhodnutí/dead-end *bez* uvedeného důvodu, model se **jednou doptá** na why (decline/self-evident → uloží bez něj). Záměrně úzká varianta Claude memory.md vzoru, který why přidává jen u feedback/project, ne u reference/faktu. Žádné `Why:` bloky ani few-shot příklady — silné Ollama Cloud / OpenRouter modely zvládnou hranici fakt-vs-rozhodnutí zero-shot. Plný kontext: history.md 2026-06-06.
|
||
|
||
**Gotcha — BOOTSTRAP_FILES jsou hardcoded:** `nanobot/agent/context.py:25` má `BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"]` — nelze přidat vlastní soubor bez patche. Vše, co má být vidět každý tah bez on-demand loadingu, musí být reference v existujícím bootstrap souboru (USER.md, SOUL.md, …).
|
||
|
||
---
|
||
|
||
## Skill `/note` — osobní znalostní báze (capture → compile, od 2026-07-01)
|
||
|
||
Přepsáno z SQLite row-store na **capture → compile pipeline** (vzor llm-wiki/detach, ale lehčí: jeden dokument, žádný index/graph/lint). Plná historie: history 2026-07-01. Plán: [plans/note-prepis.md](plans/note-prepis.md).
|
||
|
||
- **Úložiště (bez DB):** `notes/notes.md` = JEDEN strukturovaný dokument s tematickými sekcemi (`##`), které řídí LLM. Fronta zachytů = soubory v `notes/inbox/`, po zpracování → `notes/done/`, zadržené (paywall/nečitelné) → `notes/hard/`. Audit každého zachytu v `log/note.log`.
|
||
- **Dual-mode:** `/note <text>` = **okamžitě** (capture + inline compile v témže tahu, default); `/note cron <text>` = **odloženě** (jen capture, zpracuje cron). Oba volají `note_capture.py` (dumb atomic zápis) + sdílený *compile workflow* v SKILL.md + sdílený `notes/.compile.lock`.
|
||
- **Compile:** `note_compile.py` (systémový crontab uživatele `nanobot`, každou minutu) — levný fs pre-check bez importu nanobota, lockfile, `Nanobot.from_config().run(DRAIN_GOAL)` (process_direct bez cron preamble). Reformuluje na terse fakta, URL stáhne přes `web` tool + detekce paywallu, zařadí do sekce, přesune zdroj do `done/`/`hard/`.
|
||
- **Search:** `/note search|find <dotaz>` → načti celý `notes.md`, odpověz (read-only). Mazání = editace dokumentu (žádné display ID, žádné tagy).
|
||
- **Odděleno** od `/keep`, `MEMORY.md`, Dream, llm-wiki (`cml/`). **Kolize triggerů s `keep`:** NL „poznamenej si/ulož si …" může spadnout do `keep` (obě to claimují) — spolehlivý je explicitní `/note` prefix; description note skillu odlišen („NOT durable facts → keep"). Starý `note.py`/`note.sqlite` vyřazeny (`note.sqlite` v `backup/note-retired/`).
|
||
|
||
---
|
||
|
||
## Skill `/project` — pojmenované dlouhodobé pracovní kontexty (přepsáno 2026-07-22)
|
||
|
||
Substituce Claude.ai "Projects". Adresář na projekt, ne jeden soubor: `workspace/projects/<slug>/{prompt.md, memory.md, state.md, artifacts/}`. `prompt.md` = kontext/instrukce čtené při aktivaci; `memory.md` = append-only chronologická historie/rozhodnutí; `state.md` = **živý** dokument (přepisuje se na místě, syntéza "kde to teď je" — ne deník); `artifacts/` = generované soubory bez zvláštní evidence.
|
||
|
||
**Aktivní projekt = čistě konverzační paměť**, žádný stavový soubor na disku pro "co je aktivní" (viz [[decisions.md]]). Nový projekt vzniká jen po explicitním potvrzení uživatele — skill bez ptaní pracuje jen s existujícími projekty. Mazání/přejmenování projektu je mimo scope v1 (ruční zásah v adresáři).
|
||
|
||
**Nahradil netrackovaný server-side skill** (existoval na serveru mimo tento repo, žádná zmínka v history/knowledge/decisions před tímto datem): plochý soubor `projects/<slug>.md` s frontmatterem `status`/`priority`/`created`, CLI backend (`scripts/project.py`: add/list/show/status), `switch` ukládal aktivní projekt do `my` scratchpad nástroje (viz níže). Reálná data (`projects/radio-1.md`, projekt na stříhání audio streamu Radia 1, 2026-06-09) přemigrována do nového formátu jako `projects/radio1/`. Plná historie: history.md 2026-07-22.
|
||
|
||
## `my` nástroj — přece jen nějaký perzistentní scratchpad existuje
|
||
|
||
Zjištěno 2026-07-22 při objevu starého `/project` skillu výše: ten používal `my(action="set", key="project_context", value="<slug>")` k uložení aktivního projektu (a zjevně `action="get"` k přečtení). To je v napětí s dřívějším závěrem [[Bound cron job sdílí session s chatem]] výše, že nanobot nemá žádný session-scoped state kromě historie zpráv — `my` je zjevně nějaká forma key-value scratchpadu dostupná agentovi jako tool.
|
||
|
||
**Needs-verify:** starý skill sám dokumentoval jen „lives only in `my` scratchpad and is lost on restart" — neříká, jestli je klíč per-`session_key` nebo globální napříč celým agentem. Nebylo ověřeno proti zdrojáku (`nanobot/agent/tools/*.py`), jen pozorováno z použití v `SKILL.md` starého skillu (kopie stažená do `tmp/server-project-check/`, negitované). Pokud je globální, nemusí řešit "aktivní projekt per konverzace" o nic líp než čistě konverzační paměť — proto zůstal nový `/project` skill navržen bez `my`. Stojí za ověření zdrojáku, než se `my` použije jinde jako spolehlivý state store.
|
||
|
||
## Nanobot MÁ `web` tool (search + Jina Reader fetch) — `TOOLS.md` ho nezmiňuje
|
||
|
||
Config `tools.web` (server `config.json`) má `enable: true` s **search** (`provider: duckduckgo`, maxResults 5) a **fetch** (`useJinaReader: true`). Agent tedy umí stáhnout URL přes `web` tool — Jina Reader vrací čistý markdown a zvládne JS i měkké paywally. `workspace/TOOLS.md` (dokumentace toolů) `web` **neuvádí** (jen `exec`/`grep`/`cron`) — je neúplný; zdroj pravdy je `config.json`. Pro fetch URL ve skillu preferuj `web` tool; `exec`+curl (`-o file` kvůli 10k/60s limitu `exec`) je fallback. Ověřeno při přepisu `/note` (DT Glass URL reálně stažena, history 2026-07-01).
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
**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_<id>.jsonl` (kopie i v `~/.nanobot/webui/`). Pro „proč model udělal X" stáhnout příslušnou session a číst turny — odhalí, že příkaz uspěl a chyba je až v prezentaci.
|