Files
nanobot-runtime/develop/knowledge.md
2026-09-16 08:56:28 +02:00

127 KiB
Raw Blame History

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.jsonproviders 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:

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ř.:

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 turnbez jakéhokoli delivery gatingu (na rozdíl od heartbeat, který volá evaluate_response(..., default_notify=False) s fail-closed chováním). Cokoli agent v bound cron tahu odpoví, jde přímo do kanálu.

session_key (kam se zapíše historie) je nezávislý na origin_channel/origin_chat_id (kam se doručí)origin_delivery_context() (nanobot/cron/session_delivery.py) čte výhradně payload.origin_channel/origin_chat_id, vůbec ne session_key. Lze tedy nastavit session_key na cokoli (jiná/izolovaná session, nebo — pokud jde mimo jobs.json — per-run unikátní hodnota) a doručení do daného kanálu/chatu zůstane funkční.

Gotcha: jobs.json neumí dynamický/per-run session_key (je to statický string v definici jobu) — takže "vždy čerstvá session" se nedá vyřešit uvnitř jobs.json, i když by teoreticky šlo nastavit fixní odlišnou session (izolovanou od živého chatu, ale pořád akumulující historii mezi běhy). Pro opravdu čistý kontext při každém běhu je potřeba jít mimo jobs.json úplně — Python API Nanobot.run(session_key=…) s novou hodnotou (např. timestamp) při každém spuštění, vzor detach (f"detach:{path.stem}"). Nanobot.run(ephemeral=True) (nanobot.py:126-135, process_direct(ephemeral=True) interně) existuje, ale neprochází channel-delivery pipeline — po ephemeral běhu by bylo nutné doručení řešit ručně (publish OutboundMessage nebo přímé Bot API volání).

Praktický důsledek: Cron job navázaný session_key na stejnou session jako živý chat uživatele (f"{channel}:{chat_id}", typicky vznikne, když se job zakládá přímo z chatu) kontaminuje conversation historii mezi denním chatem a nočními běhy navzájem — omyl agenta v jednom běhu se táhne do dalších. Řešeno pro compact-memory-auto-daily: history 2026-07-15.

Zdroj: nanobot/cron/session_turns.py:is_bound_cron_job, nanobot/cron/bound_runner.py:run_bound_cron_job, nanobot/cron/session_delivery.py:origin_delivery_context, nanobot/nanobot.py:126-165 (Nanobot.run).


Workspace vzniká při prvním spuštění agenta

~/.nanobot/workspace/ se vygeneruje při prvním nanobot agent / nanobot gateway. Obsahuje AGENTS.md, USER.md, SOUL.md, HEARTBEAT.md, memory/, git store. (Do 0.2.x i TOOLS.md — od 0.3.0 už ne, viz níže.)

Co se auto-loaduje do system promptu (verze 0.3.0)

Každý tah ContextBuilder skládá system prompt z těchto zdrojů (žádná cache, fresh read_text()):

  • Bootstrap files v rootu ~/.nanobot/workspace/: AGENTS.md, SOUL.md, USER.md. Po editaci není potřeba restart service — změna platí od příští zprávy.
    • Zdroj: nanobot/agent/context.py:57 (BOOTSTRAP_FILES), context.py (_load_bootstrap_files).
  • agent/tool_contract.md z balíčku — kontrakt toolů se od 0.3.0 rendruje přímo do promptu (parts.append(render_template("agent/tool_contract.md"))), needitovatelný a vždy přítomný. Dřív to byl workspace soubor TOOLS.md; upstream commit d29fcaf5 (21. 5. 2026) ho přesunul do balíčku jako templates/agent/tool_contract.md. Pokrývá general tool contract, discovery/grep, file workflows, exec, CLI apps, web, messaging, scheduling — vlastní verze těchhle témat ve workspace jsou tedy duplicita.
  • memory/MEMORY.md — hardcoded cesta v MemoryStore. Žádný jiný soubor v memory/ se NEčte (ani .bak, ani user-vytvořené .md). history.jsonl konzumuje výhradně Dream procesor.
    • Zdroj: nanobot/agent/memory.py:55 (memory_file = memory_dir / "MEMORY.md"), memory.py:205,229.
  • Skilly s metadata.always: true ve frontmatteru workspace/skills/<name>/SKILL.md — přes SkillsLoader.get_always_skills(). Ostatní skilly se nahrávají on-demand, ne do system promptu.
    • Zdroj: nanobot/agent/skills.py:203.

HEARTBEAT.md není v system promptu každého tahu — má vlastní mechanismus přes heartbeat/service.py, čte se jen na heartbeat tick (default 30 min).

Důsledek: Když agent v chatu vytvoří soubor v memory/ mimo MEMORY.md (např. memory/film_policy.md), tváří se jako že si pravidlo „uložil", ale agent ho v dalším tahu neuvidí. Místo toho ho musí jít do bootstrap souboru — viz následující sekce.

K čemu slouží jednotlivé workspace soubory

Soubor Doména Co tam patří Co tam nepatří
SOUL.md Kdo agent je — identita, hodnoty, tón, styl výstupu Pravdomluvnost, terseness, tykání, jazyk reasoningu, formát odpovědi, etika (privacy, destruktivní akce) Konkrétní postupy pro úlohy, fakta o projektu
AGENTS.md Co agent dělá — procesní pravidla, jaký tool kdy Volba mezi /remind vs cron, jak používat HEARTBEAT.md, varování typu „nepiš reminder do MEMORY.md" Identita, hodnoty, fakta o uživateli
USER.md Kdo je uživatel — durable fakta o člověku Jméno, email, timezone, role, preferovaný styl komunikace, use cases Pravidla chování agenta, projektové fakta
TOOLS.md Od 0.3.0 se nenačítá — nahradil ho agent/tool_contract.md uvnitř balíčku. Náš unikátní obsah (python — use uv, Doručené připomínky) přesunut do AGENTS.md 2026-08-01, soubor smazán.
memory/MEMORY.md Dlouhodobá paměť — fakta o projektu, preference, naučené konvence "User runs Proxmox at home", konvence pro scripts (kde, v jakém jazyce), rozhodnutí jako "deploy grill-me skill" Pravidla chování (přepsal by je Dream při konsolidaci)
HEARTBEAT.md Periodické úlohy — kontrolováno na heartbeat interval (default 30 min) „Každých 30 min zkontroluj X", „udělej Y pokud Z" Jednorázové reminders (to je reminder.yaml přes /remind)

Test umístění (rozhoduj podle otázky, ne podle obsahu pravidla): „Mění to kdo jsem (SOUL) / co dělám (AGENTS) / kdo je uživatel (USER) / jak používám tool (TOOLS) / co vím o projektu (MEMORY) / co dělám pravidelně (HEARTBEAT)?"

Zdroj: upstream nanobot/templates/{AGENTS,SOUL,USER}.md (header docstrings), nanobot/agent/context.py, nanobot/agent/memory.py, nanobot/heartbeat/service.py.

Ollama provider potřebuje /v1 suffix v apiBase

Nanobot volá OpenAI-kompatibilní /v1/chat/completions, ne Ollama-native /api/chat. V configu musí být apiBase: http://host:11434/v1 — bez /v1 vrací Ollama 404.

docs/configuration.md to v příkladu (http://localhost:11434) neuvádí — je to zavádějící.

modelPresets = jeden agent, víc modelů

Nanobot nepodporuje víc pojmenovaných agentů. Místo toho má modelPresets — pojmenované dvojice (provider, model), mezi kterými se přepíná za běhu příkazem /model <preset> v chatu (Telegram i WebUI). Default je agents.defaults.modelPreset.

Gateway s websocket.host: 0.0.0.0 bez tokenu odmítne start

Bezpečnostní pojistka — pokud má WebSocket channel host: 0.0.0.0 (bind všech rozhraní), vyžaduje vyplněný token. Jinak gateway selže při startu.

Porty gateway

Port Co tam je
8765 WebUI HTML (SPA) + WebSocket auth endpoint na stejném portu
18790 Gateway health endpoint (/health{"status":"ok"})

CLI chat mód: nanobot agent

Interaktivní konverzace s agentem přímo v terminálu se spouští příkazem nanobot agent. Je to stejný agent jako přes Telegram/WebUI a sahá do stejného ~/.nanobot/workspace/ (sdílí paměť, bootstrap soubory i git store). Fungují v něm i slash-příkazy (/model <preset>, /restart, /history, /status, /goal, …).

Přehled CLI módů: nanobot onboard (setup wizard), nanobot agent (chat v terminálu), nanobot gateway (WebSocket gateway pro WebUI/Telegram).

Zdroj: upstream HKUDS/nanobot Quick Start („3. Chat: nanobot agent").

/model bez argumentu vypíše dostupné presety

V chatu (CLI nanobot agent / Telegram / WebUI) napsání samotného /model (bez argumentu) vypíše status: aktuální model, aktuální preset a seznam dostupných presetů. /model <preset> přepne. Stejný seznam se ukáže i při pokusu přepnout na neexistující preset.

Seznam ukazuje nakonfigurované modelPresets z ~/.nanobot/config.json, ne katalog modelů, co provider reálně nabízí (na to viz Ollama …/api/tags, OpenRouter …/api/v1/models). OpenAI-kompatibilní endpoint /v1/models vrací taktéž jen presety.

Zdroj: nanobot/command/builtin.py (cmd_model, _model_command_status).

Telegram bot commands — /new resetuje session, /restart ne

V Telegramu jsou slash-příkazy zaregistrované jako BotCommand (objeví se v menu po stisku / v inputu). Nejsou to volné texty pro agenta — regex router (_forward_command) je posílá rovnou do AgentLoop, agent je v promptu nevidí.

Příkaz Co dělá
/new Reset session. Zruší aktivní task, vyprázdní zprávy v sessionu, snapshot pošle do Consolidatoru na archivaci na pozadí. Tohle je „clear context" před novou diskuzí.
/restart Restartuje bota (proces), ne session — po restartu konverzace pokračuje. Slouží k načtení nové konfigurace, ne k čistění kontextu.
/stop Zruší aktuálně běžící task, kontext nechá.
/history Vypíše posledních N zpráv (read-only).
/status, /goal, /pairing, /model, /dream, /dream_log, /dream_restore, /help Ostatní registrované commands.

Pozn. k aliasům: Telegram nepovoluje pomlčku v command jménu, takže /dream_log a /dream_restore jsou aliasy — handler je interně přemapuje na kanonické /dream-log a /dream-restore (_normalize_telegram_command).

Zdroj: nanobot/channels/telegram.py:258-326 (BotCommand registrace, regex router, alias normalizace), nanobot/command/builtin.py:199 (cmd_newsession.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.

Týdenní náhodný režim (period='week', od 2026-06-15): --random-times-per-week N (mutually exclusive s --random-times-per-day) rozprostře N odpálení přes týden PoNe na N různých náhodných dnů, 1 čas na den uvnitř okna. Engine zůstává per-day volaný_weekly_fire_times spočítá celý týdenní plán deterministicky ze seedu f"{pondělí}|{text}|week" (ne ze dne) a vrátí jen fires daného dne, takže daemon i forecast nemění iterační logiku. Sloupec schedule_random.period ('day'/'week', default 'day') + idempotentní _migrate(conn) v db.get_db (PRAGMA-guarded ALTER TABLE ADD COLUMN, protože init_db na živou DB nesahá). count > dostupné dny se clampuje (hraniční týden u from/until), count > kapacita filtru raisuje. Detaily: history 2026-06-15.

Display IDs (autonomní serverová feature, do repa dotaženo 2026-06-15): list/upcoming ukazují 1-based pozici mezi aktivními remindery (#1, #2…, počítáno on-the-fly v _active_display_order), ne interní DB id. --id i chybové hlášky ({"error":"no match","display_id":n}, ambiguous s display_id) pracují s display ID. Přečíslovává se po každém remove → při nejistotě vždy nejdřív list. Interní id se uživateli nikdy neukazuje. (Objeveno při syncu před weekly deployem — feature byla na serveru, ne v repu; pravděpodobně Dream procesor. history 2026-06-15.)

uv path na serveru: /home/nanobot/.local/bin/uv — není v PATH pro root. Spouštět jako /home/nanobot/.local/bin/uv run script.py.

DB_PATH path (gotcha): V remind_send.py/remind_cli.py je Path(__file__).resolve().parent.parent.parent.parent4 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émreminder_fires.schedule_type se pro cron/random zapisoval špatně ('at') a dedup pro ně nefungoval (maskovala jen 60s tolerance). Příčinaschedule_{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 --keywordambiguous 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):

    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:

    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. Aktuální sada (ověřeno 2026-09-16): glm, flash, glm52, glm51, kimi, kimi26, kimi3, sonnet, haiku, gemini-flash, gemini-flash-lite. Nepojmenovaný alias = aktuální generace (glm = glm-5.3, kimi = kimi-k2.7-code), číslovaný = zamrzlá starší verze. (Dřív tu stálo <model>-<provider> jako kimi-k2.6-openrouter, pak glm53/kimi27; 2026-09-16 přejmenováno na současnou formu.)

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]].

Gotcha — přejmenování presetu validace nechytí. Config._validate_model_preset (nanobot config/schema.py, model_validator mode="after") ověřuje při startu jen agents.defaults.modelPreset a fallbackModels. Preset předaný ad-hoc do Nanobot.from_config(model_preset=...) neprojde žádnou statickou kontrolou a spadne až za běhu v Config.resolve_preset() — u nočních cron jobů tedy ve 2:00 do logu a Telegramu, ne při editaci configu. Po každém přejmenování presetu projdi tahle místa:

  • skills/reflect/scripts/reflect_auto.pyMODEL_PRESET
  • skills/compact-memory/scripts/compact_memory_auto.pyMODEL_PRESET
  • serverový ~/.nanobot/workspace/knowledge/models.md — tabulka presetů, podle které si agent vybírá /model; neplatný alias tam je tichá chyba (agent zkusí neexistující preset)
  • skills/detach/ řešit nemusíš — preset resolvuje dynamicky přes load_preset_names()/resolve_preset()

Jednorázový check: ssh nanobot@nanobot.hell 'grep -rn -e <starý-alias> ~/.nanobot/workspace/skills/ ~/.nanobot/workspace/knowledge/ ~/.nanobot/config.json' (pozor na __pycache__ — zastaralý .pyc matchne, ale nic neovlivní). Automatický validátor uživatel 2026-09-16 explicitně zamítl: Telegram alert z nočního běhu je jako detekce dostatečný. Plný záznam: history 2026-09-16.

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:

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 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, ollama.com/library/kimi-k3, ověřeno 2026-07-27 (history).

Logování: gateway -v/--verbose, agent --logs

Nanobot defaultně vypíná vlastní logy (logger.disable("nanobot")), proto v běžném výstupu nic není. Zapínají se podle příkazu:

  • nanobot gateway -v / --verbose → INFO+DEBUG do stderr (u nás přes systemd do journalu). Pokrývá i WebUI — WebUI je jen websocket channel uvnitř gateway procesu (port 8765), není to samostatná služba, takže žádný separátní přepínač pro WebUI neexistuje.
  • nanobot agent --logs / --no-logs → runtime log přímo v interaktivním CLI chatu. Jiný flag než gateway, nejsou zaměnitelné.

Žádná env proměnná ani config klíč pro log level mimo tyhle flagy neexistuje.

Nasazení u nás: -v bylo v ExecStart v ~/.config/systemd/user/nanobot.service na nanobot.hell, ale už tam není (ověřeno 2026-09-15: ExecStart=/home/nanobot/.local/bin/nanobot gateway, poslední LLM usage řádek v journalu je z 2026-05-27). Bez -v tedy tokeny nikde nejsouProcessing message/Response to jsou INFO a logují se dál, LLM usage je DEBUG a ne. Logy živě: ssh nanobot@nanobot.hell 'journalctl --user -u nanobot.service -f --no-pager'.

Co -v ukáže v jednom tahu (ověřeno na WebUI zprávě):

  • Processing message from <channel>:<id>: <text> — příchozí zpráva
  • stavy agentního tahu: RESTORE → COMPACT → COMMAND → BUILD → RUN → SAVE → RESPOND (každý s časem)
  • Tool call: <nástroj>({...args...})volání toolu i s argumenty (INFO)
  • LLM usage: prompt=… completion=… cached=… — spotřeba tokenů každé iterace agentní smyčky
  • Response to <channel>:<id>: <text> — finální odpověď

Co se NEloguje: tělo tool výsledku (stdout), plné LLM zprávy ani thinking. Thinking jde samostatným kanálem do klienta (WebUI), ne do journalu. -v je serverová záležitost — ve WebUI se nic nezmění.

Pozor: -v zapíná INFO+DEBUG globálně, takže v journalu jsou i heartbeat/cron/dream tahy.

Startup taky vypíše užitečné: Registered N tools: [...] (výčet dostupných toolů) a Runtime model switched … <model> (aktivní preset).

Srovnání modelů pro nanobot (cloud inference)

Hodnoceno pro mix: agentní úlohy (tool use, Dream, skilly) + rychlost + Python. Platí pro cloud Ollama i OpenRouter — hardwarové podmínky jsou srovnatelné. Provider-agnostic pohled (předpokládá dostupnost rychlé inference).

Za podmínky Ollama Cloud (žádný rychlý provider) to upřesňuje models.md — tam rozhoduje latence, takže pro interaktivní vrstvu vede GLM-5.1, ne Kimi. Tahle tabulka a models.md se nerozcházejí v datech, jen v východisku: provider-agnostic vs. fixní Ollama Cloud.

Pořadí Model Proč
1 Kimi K2 (kimi-k2.6-*) Jediný explicitně trénovaný na agentní úlohy a tool use; MoE ~32B aktivních params = rychlý
2 Qwen 3.6+ (qwen-3.6-plus-openrouter) Pravděpodobně Qwen3 235B-A22B (~22B aktivních = nejrychlejší v seznamu); top coding, silné instruction following
3 DeepSeek V3.2 (deepseek-v3.2-ollama) Nejlepší Python, nejsilnější instruction following; ~37B aktivních; ideální pro Dream
4 Qwen 3.5 (qwen3.5-ollama) Solidní záloha, dobrý coding, rychlý
5 GLM-5.1 (glm-5.1-ollama) Dobrý model, ale za Kimi/Qwen/DeepSeek na všech osách
67 MiniMax M2 (obě varianty) Nejméně prověřený pro agentic workload; rezerva pro speciální případy

Prakticky: primary model → kimi-k2.6; Dream (pokud chceš jiný preset) → deepseek-v3.2 nebo qwen-3.6-plus.

Jak funguje nanobot skill systém

Skill = složka ~/.nanobot/workspace/skills/<name>/ se souborem SKILL.md (YAML frontmatter s name + description, tělo markdown instrukce). Bootstrap soubory se čtou při každém tahu bez restartu. Žádný install příkaz neexistuje — skill se vytvoří ručně (nebo ho Dream vytvoří sám).

Clawhub.ai / OpenClaw je jiný ekosystém, nemá s nanobotem nic společného. Skilly odtud je třeba manuálně adaptovat.

Claude Code skilly jsou přímo přenositelné. Anthropic Skills format (SKILL.md s YAML frontmatter name+description + markdown tělo) je identický s nanobot skill formátem. Stačí zkopírovat složku skills/<name>/ ze zdroje (např. plugin .claude-plugin/skills/<name>/) do ~/.nanobot/workspace/skills/<name>/ — žádná konverze. Manifest .claude-plugin/plugin.json se neinstaluje, je Claude-Code-specific. Pozor jen na (a) reference na Claude-Code tooly v těle skillu (TodoWrite, ExitPlanMode, AskUserQuestion apod. v nanobotovi neexistují), (b) prompt-injection v markdown těle — nanobot čte skill jako součást system contextu. Ověřeno: nasazen grill-me z mattpocock pluginu (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řesread_file`. Důsledky:

  • description je jediná info o skillu v promptu, dokud agent nečte tělo → patří tam jen kdy/proč skill spustit (trigger fráze, odlišení od příbuzných skillů), ne jak funguje.
  • description se nezkracuje (_get_skill_description vrací text doslova) a je v promptu každý tah u všech skillů → trvalý token cost. Drž stručně, routing-orientovaně. Detailní postup patří do těla.
  • Always skilly (metadata.nanobot.always: true): description se ignoruje úplně, do promptu se eager vkládá celé tělo (bez frontmatteru). Druhý vysvětlující odstavec v description je u nich čistý šum.

Co tedy patří do description: jen kdy/proč skill spustit — krátká věta o účelu + trigger fráze + případné odlišení od příbuzného skillu. Nepatří tam jak skill funguje (to do těla, čte se on-demand) ani detailní postup. Triggery nemusí být dvojjazyčné — model rozpozná záměr napříč jazyky, takže explicitní CZ varianty nic nepřidají, jen prodlužují řádek (ověřeno na /plan, 2026-05-31).

Zdroj: nanobot/agent/skills.py:111-159 (build_skills_summary, _get_skill_description), skills.py:94-109 (load_skills_for_context, always skilly), nanobot/agent/context.py:87-95.

Oficiální tvar description (Anthropic — Skill authoring best practices, platí i pro nanobot, formát je identický). Nanobot wiki frontmatter skillů nedokumentuje — autoritou je tenhle dokument plus zjištění ze zdrojáku výše.

  • Tvar: <co skill dělá, slovesná fráze>. Use when <triggery/kontexty>. Např. Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
  • Vždy třetí osobaProcesses Excel files…, nikdy I can help you… ani You can use this to…; nekonzistentní osoba zhoršuje discovery.
  • Musí obsahovat co dělá i kdy použít, klíčový use case první, konkrétní klíčové termíny. Vágní (Helps with documents) je anti-pattern.
  • Limit 1 024 znaků (Agent Skills spec); Claude Code listing ořezává na 1 536.
  • Nanobotí dodatek k tomu: „co dělá" = schopnost, ne postup. Adds, deduplicates, and compacts entries in keep.md je jak → do těla.

Aplikováno na keep (history 2026-09-04).

Dream procesor — automatické self-improvement

Nanobot má vestavěný Dream procesor (agent/memory.py:Dream) který běží každé 2 hodiny. Jde o dvou-fázový LLM pipeline nad history.jsonl:

  • Fáze 1: Plain LLM call analyzuje historii, hledá fakta ([MEMORY]/[USER]/[SOUL]), kandidáty na smazání ([FILE-REMOVE]), opakující se workflow ([SKILL])
  • Fáze 2: AgentRunner s read_file/edit_file/write_file tools provede chirurgické editace; umí sám vytvářet nové skilly (skills/<name>/SKILL.md)

Dream řeší: deuplikaci, detekci stale obsahu (git blame age na řádcích MEMORY.md), automatické git commity po změnách. Cursor v .dream_cursor zabraňuje přepracování.

Důsledek: Self-improving-agent skilly z jiných ekosystémů jsou z velké části redundantní — Dream pokrývá jejich core funkcionalitu nativně. Přidaná hodnota by byl jen okamžitý strukturovaný error log (ERR-YYYYMMDD-XXX formát) — Dream čeká 2h.

Zdroj: nanobot/agent/memory.py:Dream, prompt templates agent/dream_phase1.md, agent/dream_phase2.md

Kam Dream smí zapisovat (ověřeno ve zdrojáku 2026-09-02): build_dream_tools() (nanobot/agent/memory.py:641) registruje Edit/Write/ApplyPatch s allowed_dir = workspace/skills + extra_write_allowed_files = [memory/MEMORY.md, SOUL.md, USER.md]. Číst smí celý workspace (ReadFileTool s allowed_dir = workspace).

Dva důsledky:

  1. Do workspace/projects/ (ani jinam mimo skills/) Dream zapsat nemůže — je to vynucené kódem, ne promptem. Psát takový zákaz do těla skillu je zbytečné (a Dream tělo non-always skillu stejně nečte).
  2. Serverové skilly mohou driftovat proti repu bez našeho zásahu — Dream do skills/ zapisovat smí. Doloženo: skills/project/SKILL.md na serveru se 2026-09-01 13:15 sám změnil (přibyla sekce o dělbě prompt.md vs state.md), zatímco repo mělo verzi z 07-22. Stejný jev už dřív u /remind display IDs (viz výše). Proto vždy nejdřív stáhni serverovou verzi a porovnej, než skill přepíšeš.

Non-interactive nanobot CLI: streamuje chaoticky, Python API vrací čistý string

nanobot agent --message "..." --session "..." projede agent loop, výstup ale streamuje rozkouskovaně přes stdout ( prefixované delty reasoning/progress, finální response.content až na úplném konci). I při --no-markdown a pipe (| cat) jde streaming dál. Postprocesovat by bylo křehké.

Pro programatické použití (daemon, skript) jdi přes Python API:

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í sessionpayload.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_keysession.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 "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"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 — ř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 (open od 2026-03-25) řeší podobný problém na starším kódu, pravděpodobně obsoletní vůči #4550.

Souvisí: PR #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 " → 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/ v tracking repu, history 2026-05-28 „Skill detach + daemon" + iterace #2 + iterace #3.

uv-native invokace (iterace #3): Shebang přepnut na #!/usr/bin/env -S uv run --script s PEP 723 inline metadata (requires-python = ">=3.11", dependencies = ["nanobot-ai"]). uv run --script samo vytvoří/cachuje izolované venv — skript přežije uv tool uninstall/install i přesun na jiný stroj. První spuštění po PEP 723 změně trvá ~5-10s (budování venv), další jsou instantní (cache v ~/.cache/uv/). Systemd user unit musí mít Environment=PATH=%h/.local/bin:/usr/bin:/bin, jinak uv v PATH chybí.


Detach notifikace do origin kanálu (WebUI/CLI) — záměrně nepodporováno

Daemon notifikuje jen Telegram (přes Bot API, deterministicky). Když task přišel z WebUI nebo CLI, do toho kanálu se notifikace nepošle — uživatel si výsledek vyzvedne přes výsledek <slug> (detach subaction read).

Architektonický důvod: WebSocket spojení vlastní gateway proces; daemon je samostatný systemd oneshot. Nanobot nemá HTTP endpoint pro vstřikování zpráv do WS sessions (nanobot/channels/websocket.py:673-782 — všechny /api/sessions/... jsou read-only). Sdílí jen filesystem, žádné IPC.

Zvážené a zamítnuté možnosti:

  • Samostatný Nanobot.run() jen kvůli notifikaci — LLM jako IPC proxy. Pomalé (1030 s), drahé, nedeterministické (model může prompt překroutit nebo message tool nezavolat). Stejná třída problému jako Cron job s LLM agentem je nespolehlivý.
  • Přibalit message tool call k existujícímu agent turnu tasku — žádný extra LLM call, ale stále LLM-mediated; nepokrývá timeout/exception (agent se k toolu nedostane).
  • Patch upstream + nový HTTP endpoint na gatewayi — čisté řešení (daemon dělá prostý POST, žádný LLM), ale udržovat patch napříč upgrady nanobot-ai. Pokud někdy ano, místo je nanobot/channels/websocket.py (přidat handler vedle stávajících /api/sessions/..., vytvořit OutboundMessage(channel="websocket", chat_id=..., content=...) a bus.publish_outbound(msg)).

Rozhodnutí 2026-05-29: status quo — Telegram fallback stačí, výsledek <slug> je dokumentovaný způsob pro WebUI/CLI.


Exec safety guard shazuje český text — diakritika + dvojtečka = „windowsová cesta"

Problém → příčina → pravidlo. exec vrátí Command blocked by safety guard (path outside working dir) u příkazu, který obsahuje běžnou českou prózu. Příčina: ExecTool._extract_absolute_paths() (agent/tools/shell.py) hledá windowsové cesty regexem (?<![A-Za-z])(?:[A-Za-z]:[^\s"'|><;]*|…). Lookbehind je ASCII-only, takže znak s diakritikou před ASCII písmenem ho neutne — Cíl: dá token l:, Závěr:/směr: dají r:. Path("r:").resolve() to rozvine vůči cwd démona (/home/nanobot, ne vůči workdiru příkazu) na /home/nanobot/r: → mimo workspace → blok. Ověřeno 2026-09-11 spuštěním nainstalovaného guardu na reálných příkazech.

Guard jede nad raw command stringem, bez shell parseru (_split_shell_segments se používá jen na allow/deny patterns). Nerozliší tedy argument od obsahu heredocu — heredoc, --text i printf | pipe s týmž textem padnou identicky. Citování a quoted heredoc nepomáhají.

Pravidlo pro psaní skillů: český text nikdy nedávej do command stringu. Vždy write_file do tmp/ + předání cesty (--file <path>, nebo < tmp/soubor). Cesta v příkazu je neškodná; relativní cesty žádný z regexů nechytá.

Pravidlo je od 2026-09-11 zapsané globálně v workspace/AGENTS.md, sekce ## exec Tool — tedy v system promptu při každém tahu, nezávisle na tom, jaký skill se triggerne. AGENTS.md je jediný z BOOTSTRAP_FILES (agent/context.py:57), který je user-ownovaný; SOUL.md, USER.md i memory/MEMORY.md přepisuje Dream a bundled templates/ přepíše upgrade balíčku. Dvě omezení: (1) AGENTS.md se bere z project_root aktuálního tahu (context.py:163), takže v session scoped do tmp/<x> se root verze nenačte — pro běžný chat platí; (2) kdyby byl obsah identický s templates/AGENTS.md, soubor se do promptu tiše nedá (context.py:179182). Tentýž guard byl v AGENTS.md předtím zdokumentovaný dvakrát izolovaně (sekce ## Git commit timestamps o date '+%H:%M:%S' → token H:%M:%S, a původní věta o chybějícím workspace path), aniž by se pojmenovala společná příčina.

Co blokuje a co ne (ASCII písmeno + :, kde znak před písmenem není ASCII písmeno):

Blokuje Projde
Cíl:, Závěr:, směr:, díl: Úkol:, Stav:, Řešení:, Otázky:, Poznámka:

Pozor i na druhý guard nad raw stringem: if "..\\" in cmd or "../" in cmd → jakákoli prozaická zmínka ../ shodí příkaz na path traversal detected.

Stav skillů. Opraveno na --file: project (log --file) a note (note_capture.py --file, 2026-09-11 — byl nejrizikovější, protože bere vstup uživatele doslova, takže poznámka „Cíl: …" tiše selhala). Zbývá dluh: bookmark (český článek v heredocu) a remind (edit --text "…"). Upstream regex zůstává rozbitý — fix by chtěl unicode-aware lookbehind.


Prostředí exec toolu — PATH z procesu tam nedosáhne

ExecTool._build_env() (agent/tools/shell.py) staví prostředí subprocessu od nuly. Na Unixu předá jen HOME, LANG, TERM, PYTHONUNBUFFERED (+ cokoli v tools.exec.allowedEnvKeys). PATH se z os.environ nekopíruje — na Windows ano, na Unixu ne. Cokoli nastavíš v systemd unitu, ~/.profile nebo wrapperu, exec neuvidí.

Do 0.2.2 to nevadilo, protože exec běžel jako login shell (bash -lc) a ten sourcoval ~/.profile s PATH="$HOME/.local/bin:$PATH". Upstream commit 13c951aa (25. 6. 2026) přepnul default login na False — kvůli secrets, které se profilem vracely zpátky do prostředí. Bez login shellu se PATH dopočítá z vestavěného defaultu bashe:

/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.

~/.local/bin tam není → uv: command not found, exit 127.

Jediná správná cesta je tools.exec.pathPrepend / pathAppend v config.json. Hodnota musí být adresář, ne cesta k binárce — /home/nanobot/.local/bin/uv do PATH lookupu nepřispívá ničím. Implementace: _wrap_path_export() předřadí příkazu export PATH="$NANOBOT_PATH_PREPEND:$PATH"; ….

Preferuj pathPrepend. Default bashe končí . (aktuální adresář) a exec běží s CWD = workspace root, do kterého zapisuje agent i Dream — s pathAppend by . bylo v pořadí před našimi cestami.

Není hot-reload. Config watcher volá agent.invalidate_runtime_config() (agent/loop.py:516), což invaliduje jen model-runtime resolver; ExecTool.create(ctx) běží jednou při startu gateway. Po změně tools.exec.* je nutný systemctl --user restart nanobot.

Protiváha — PATH procesu inertní není. run_cli_app spouští CLI aplikace s env=os.environ.copy() (apps/cli/service.py:1372) a MCP stdio servery dědí taky. Proto Environment=PATH= v systemd unitu zůstává — jen neřeší exec.

Zdroj: agent/tools/shell.py (_build_env, _wrap_path_export, _prepare_command), upstream commit 13c951aa. Diagnóza a oprava: history 2026-08-01.


Skill exec běží z workspace rootu, ne ze skill adresáře

Když skill volá exec bez explicitního working_dir, příkaz běží s CWD = workspace root (~/.nanobot/workspace), ne v adresáři skillu. Cesty na skripty skillu proto musí být buď workspace-relativní (skills/<name>/scripts/x.py) nebo absolutní — skill-dir-relativní scripts/x.py se rozbije (resolvuje na workspace/scripts/x.py).

Zdroj: upstream nanobot/agent/tools/shell.py:148 (working_dir=ctx.workspace) + :370 (cwd = working_dir or workspace_root). Pozn.: remind SKILL.md používá workspace-relativní skills/remind/scripts/remind_cli.py (korektní); detach analogicky skills/detach/scripts/….


Python skripty na serveru — uv-native pattern (PEP 723)

Preferovaný způsob pro libovolný stand-alone Python skript v ~/.nanobot/workspace/:

#!/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:

[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, uv docs uv run --script, 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:

# tasks-daemon.path
[Path]
DirectoryNotEmpty=%h/.nanobot/workspace/tasks/inbox
Unit=tasks-daemon.service

[Install]
WantedBy=paths.target
# 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

[Unit]
StartLimitIntervalSec=1800
StartLimitBurst=20
[Service]
Restart=on-failure
RestartSec=60

Restart=on-failure + RestartSecdelay 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 .bashrcnení 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:57BOOTSTRAP_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.

  • Úložiště (bez DB): notes/notes.md = JEDEN strukturovaný dokument s tematickými sekcemi (##), které řídí LLM. Fronta zachytů = soubory v notes/inbox/, po zpracování → notes/done/, zadržené (paywall/nečitelné) → notes/hard/. Audit každého zachytu v log/note.log.
  • Dual-mode: /note <text> = okamžitě (capture + inline compile v témže tahu, default); /note cron <text> = odloženě (jen capture, zpracuje cron). Oba volají note_capture.py (dumb atomic zápis) + sdílený compile workflow v SKILL.md + sdílený notes/.compile.lock.
  • Compile: note_compile.py (systémový crontab uživatele nanobot, každou minutu) — levný fs pre-check bez importu nanobota, lockfile, Nanobot.from_config().run(DRAIN_GOAL) (process_direct bez cron preamble). Reformuluje na terse fakta, URL stáhne přes web tool + detekce paywallu, zařadí do sekce, přesune zdroj do done//hard/.
  • Search: /note search|find <dotaz> → načti celý notes.md, odpověz (read-only). Mazání = editace dokumentu (žádné display ID, žádné tagy).
  • Odděleno od /keep, MEMORY.md, Dream, llm-wiki (cml/). Kolize triggerů s keep: NL „poznamenej si/ulož si …" může spadnout do keep (obě to claimují) — spolehlivý je explicitní /note prefix; description note skillu odlišen („NOT durable facts → keep"). Starý note.py/note.sqlite vyřazeny (note.sqlite v backup/note-retired/).

Skill /project — pojmenované dlouhodobé pracovní kontexty (přepsáno 2026-07-22, skript 2026-09-02)

Substituce Claude.ai "Projects". Adresář na projekt, ne jeden soubor: workspace/projects/<slug>/{prompt.md, memory.md, state.md, artifacts/}. prompt.md = kontext/instrukce čtené při aktivaci; memory.md = append-only chronologická historie/rozhodnutí; state.md = živý dokument (přepisuje se na místě, syntéza "kde to teď je" — ne deník); artifacts/ = generované soubory bez zvláštní evidence.

Aktivní projekt = čistě konverzační paměť, žádný stavový soubor na disku pro "co je aktivní" (viz decisions.md). Nový projekt vzniká jen po explicitním potvrzení uživatele — skill bez ptaní pracuje jen s existujícími projekty. Mazání/přejmenování projektu je mimo scope v1 (ruční zásah v adresáři).

Nahradil netrackovaný server-side skill (existoval na serveru mimo tento repo, žádná zmínka v history/knowledge/decisions před tímto datem): plochý soubor projects/<slug>.md s frontmatterem status/priority/created, CLI backend (scripts/project.py: add/list/show/status), switch ukládal aktivní projekt do my scratchpad nástroje (viz níže). Reálná data (projects/radio-1.md, projekt na stříhání audio streamu Radia 1, 2026-06-09) přemigrována do nového formátu jako projects/radio1/. Plná historie: history.md 2026-07-22.

Zápis do memory.md jde výhradně přes skills/project/scripts/project_cli.py log (od 2026-09-02). Subcommandy activate / log / list / new; volat workspace-relativně uv run skills/project/scripts/project_cli.py …. Skript vlastní datum (systémové hodiny, Europe/Prague) a koncový newline — obojí model prokazatelně kazil: chata/memory.md měl dva záznamy s vymyšleným datem 2026-09-14 (zapsané 09-01) a proxmox/memory.md slepený append na předchozím řádku, protože edit_file kotva se hádala bez přečtení souboru. Text se předává výhradně souborem: write_file do tmp/ + log <slug> --file tmp/… (od 2026-09-11). Dřív to byl stdin quoted heredoc (<<'NOTE'), protože --text model prokazatelně mrzačil (v log/note.log je doložený případ, kdy argument zmršil na ...) — jenže heredoc padl na exec safety guardu, který sejme jakýkoli český text v command stringu bez ohledu na citování (viz sekce „Exec safety guard shazuje český text" výše; doloženo session 002f2196, kdy tři varianty po sobě selhaly na Diagnóza/směr:). --text je proto z CLI odstraněn, stdin zůstal jako fallback. Stav skriptu přepisuje env PROJECTS_DIR (testy). Plný kontext: history.md 2026-09-02.

Projektová data nemají strop ani konsolidacimemory.md roste neomezeně a nikdy se nekomprimuje ani nearchivuje. Velikost se řeší výhradně na straně čtení: activate nad limitem tool výsledku vypustí z výstupu nejstarší záznamy a ukáže cestu k plnému logu, soubor na disku nechá beze změny. Zamítnutá varianta: prahy 8 000 / 12 000 znaků s nabídkou konsolidace — ztráta zadaného obsahu je horší failure mode než jakákoli úspora kontextu (a čísla stála na špatném okně, viz níže).

my nástroj — přece jen nějaký perzistentní scratchpad existuje

Zjištěno 2026-07-22 při objevu starého /project skillu výše: ten používal my(action="set", key="project_context", value="<slug>") k uložení aktivního projektu (a zjevně action="get" k přečtení). To je v napětí s dřívějším závěrem Bound cron job sdílí session s chatem výše, že nanobot nemá žádný session-scoped state kromě historie zpráv — my je zjevně nějaká forma key-value scratchpadu dostupná agentovi jako tool.

Needs-verify: starý skill sám dokumentoval jen „lives only in my scratchpad and is lost on restart" — neříká, jestli je klíč per-session_key nebo globální napříč celým agentem. Nebylo ověřeno proti zdrojáku (nanobot/agent/tools/*.py), jen pozorováno z použití v SKILL.md starého skillu (kopie stažená do tmp/server-project-check/, negitované). Pokud je globální, nemusí řešit "aktivní projekt per konverzace" o nic líp než čistě konverzační paměť — proto zůstal nový /project skill navržen bez my. Stojí za ověření zdrojáku, než se my použije jinde jako spolehlivý state store.

Nanobot MÁ web tool (search + Jina Reader fetch) — TOOLS.md ho nezmiňuje

Config tools.web (server config.json) má enable: true s search (provider: duckduckgo, maxResults 5) a fetch (useJinaReader: true). Agent tedy umí stáhnout URL přes web tool — Jina Reader vrací čistý markdown a zvládne JS i měkké paywally. workspace/TOOLS.md (dokumentace toolů) web neuvádí (jen exec/grep/cron) — je neúplný; zdroj pravdy je config.json. Pro fetch URL ve skillu preferuj web tool; exec+curl (-o file kvůli 10k/60s limitu exec) je fallback. Ověřeno při přepisu /note (DT Glass URL reálně stažena, history 2026-07-01).


MCP servery v nanobotu — skrytá tokenová zátěž

Každý nakonfigurovaný MCP server přidává do system promptu svůj tool schema popis. Pro sqlite MCP to jsou ~12k tokenů, a to každý tah — bez ohledu na to, jestli tool vůbec použiješ.

U menších modelů s omezeným kontextovým oknem (typicky cloud MoE modely s efektivními ~32B params) je to zbytečné plýtvání. Přitom přímá alternativa (CLI sqlite3 přes exec, nebo Python sqlite3 stdlib přes uv run) tuto zátěž nemá a pro 95 % use-cases je dostatečná.

Pravidlo: MCP server zapojit jen pokud přidaná hodnota nad přímým přístupem výrazně převáží tokenovou cenu. Pro sqlite typicky nepřeváží.


Reasoning stream () na konzoli — channels.showReasoning

Řádky prefixované , streamované token po tokenu (✻ The, ✻ user wants, …) v nanobot agent CLI chatu nejsou debug ani chyba — je to reasoning/thinking stream modelu. Řídí ho jediný config klíč channels.show_reasoning (default true, camelCase alias showReasoning).

Vypnout: channels.showReasoning = false v ~/.nanobot/config.json. Sourozenec telegram/websocket uvnitř channels, ne uvnitř konkrétního kanálu.

  • Je to globální flag, ne per-channel. Gate čte globální channels_config.show_reasoning (nanobot/cli/commands.py:345,354), ne per-kanálový config. Nelze vypnout jen pro konzoli a nechat zapnuté ve WebUI — buď všude, nebo nikde. (Trade-off: ve WebUI se reasoning hodí při ladění „proč něco jde/nejde".)
  • Restart: CLI (nanobot agent) čte config čerstvě při startu → stačí restart sezení. Gateway/WebUI/Telegram dostávají channels přes AgentLoop.from_config() jednou při startu → restart service.
  • Žádný runtime flag nanobot agent na to není; --logs/--no-logs řídí jen loguru runtime log, ne reasoning stream.
  • Příbuzné knoby v témže bloku: sendProgress (default true, progress řádky ), sendToolHints (default false, tool-call hinty). Vykreslení na commands.py:301.

Zdroj: nanobot/config/schema.py:37-39, nanobot/cli/commands.py:301,345,354. Plný záznam: history 2026-06-01 „Vypnutí reasoning streamu".


Context window presetů: default 65k, přepis přes contextWindowTokens

Nanobot má hardcoded default context_window_tokens = 65_536 pro ModelPresetConfig i AgentDefaults (nanobot/config/schema.py:101,124). Pokud preset v config.json tuto hodnotu nepřepíše, jede model na 65k bez ohledu na to, co reálně umí. Klíč v JSON: contextWindowTokens (Base má alias_generator=to_camel + populate_by_name=True, schema.py:24 → projde camelCase i snake_case). Sourozenec maxTokens (max output) má default jen 8192.

Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5 / nemotron-3-super 262144, minimax-m2.7 204800, glm-5.1 196608, deepseek-v4-flash 1048576) + maxTokens 16384. Bez restartumodelPresets 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 (glm: 976 000, flash/glm52 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-slimuv 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 nefungujeuv 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 — k 0.2.1, preset glm-5.1. Stručně: ~8,7k system prompt (největší MEMORY.md, skills_section, SOUL.md), ~5,2k tool defs, zbytek session zprávy.

llm-wiki: zaseknutí způsobuje LLM v mechanické smyčce, ne pomalost

Při wiki lint/fix operacích se agent zasekl proto, že dělal mechanickou práci v LLM smyčce (čte stránku → hledá problém → edituje → další stránka). Správné řešení přesouvá tuto práci do deterministického skriptu — LLM rozhoduje jen tam, kde je skutečně potřeba úsudek.

Praktické důsledky pro llm-wiki:

  • Lint musí vracet (soubor, řádek, návrh opravy) — ne jen "9 broken refs" → agent nemusí číst žádnou stránku
  • --fix mód má smysl jen pro mechanické triviality (chybějící frontmatter pole ze šablony) kde není úsudek
  • Slug collision a frontmatter šablona patří do compile pipeline, ne do lintu ex post

Zamítnuté návrhy ze stejného důvodu: timeout na každý krok, "aspoň jeden edit po každém čtení" (tlačí na editaci), agentní wiki fix subcommand (opět smyčka). Viz todo.md sekce "llm-wiki skill — další zlepšení".


Optimalizovat skilly kvůli tokenům se nevyplatí

Celý blok skillů (~2,5k: skills_section 1,56k + always-skilly 0,95k) je při okně 196k jen ~1,3 % okna. Smazat on-demand skill ušetří jen popis + framing (~4075 tok/kus) → fakticky neměřitelné. Description neškrtat — je to trigger pro progressive loading (model podle něj pozná, kdy skill načíst); bez něj skill přestane fungovat, ušetříš desítky tokenů a přijdeš o funkčnost. Jediná páka jsou always: true (jdou celým tělem), ale my+memory mají být always. Větší blok jsou tool defs (5,2k, jen vypnutím toolů v configu). Závěr: skilly maž podle užitečnosti, ne kvůli tokenům; reálný strop je contextWindowTokens, ne baseline. Začalo by to dávat smysl až u desítekstovek skillů nebo velkého těla jako always. Plný rozbor: tokens-explain.md.


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). Když běží víc ollama modelů než 3 naráz, přebytečné dotazy čekají ve frontě a to čekání spadne do wall-clocku (stopky obalují jen HTTP call). Dřív (4 ollama modely bez stropu): glm median 3240 ms, jednotlivá volání qwen až 59 s. Po zavedení maxConcurrency=3 (a redukci na 3 ollama modely, takže strop zatím ani nepřekáží): glm median zpět na 1690 ms, žádné odlehlé hodnoty. SemaphoreSlim slot se navíc získává mimo stopky, takže i kdyby strop překážel, čekání na slot se do měřené latence nezapočte. OpenRouter běží na vlastní infře, strop nemá. Každý model má vlastní OpenAIClient (spojení se nesdílí) — víc spojení by nepomohlo.

Reasoning = pomalé + drahé na out tokeny: deepseek out=3487, minimax out=2317 — proto 5 s. (Dříve zavržený qwen3.5 byl extrém: out=22671 tok ≈ jako input, volání i 59 s — proto vyhozen.) haiku/gpt-nano out 550700 tok = přímý parse bez reasoningu. glm rychlý (out2,9k, ale median 1,7 s).

FAILy: jediný „FAIL" haiku = false negative v test datech (zkontrolovat pečení vs pečeni; JsonCompare porovnává text přesně, ordinálně). Ostatní modely 17/17. (Z dřívějška: gemma dělala skutečnou chybu data příští pondělí06-09 místo 06-08; gemma teď v reálném configu není.)

Teze PoC potvrzena: ~1,2k input tokenů na add (vs ~2832k přes nanobot agent loop, ~30× méně) a ~1 s wall-clock u rychlých modelů (vs ~10 s u /remind list přes agenta). Sedí s odhady z plans/remind-standalone-bot.md. Plný záznam: history 2026-06-03 „MiniLoop paralelizace".

Levné / OSS modely z OpenRouteru (změřeno 2026-06-03)

Test 5 levných OpenRouter modelů (cena $/M tok in/out), gate=3 na ollam(ě) se netýká — vše OpenRouter:

Model Cena Úspěšnost Wall median / avg out tok
mistralai/mistral-small-3.2-24b-instruct 0.075/0.20 17/17 934 / 1050 ms ⚠️ 610
google/gemma-3-27b-it 0.08/0.16 17/17 1413 / 1611 ms 608
z-ai/glm-4-32b 0.10/0.10 16/17* 1905 / 2072 ms 519
qwen/qwen3-30b-a3b-instruct-2507 0.043/0.17 16/17* 1991 / 1866 ms 588
openai/gpt-oss-120b levný 16/17 7238 / 12164 ms 3769

* false negative (slovosled se protáhnout/protáhnout se, resp. pečení/pečeni).

⚠️ mistral-small-3.2 — naměřeno s cache. Opakovaný test (2026-06-03) ukázal reálné časy 3 4007 400 ms na prvních 4 příkladech, tj. ~38× horší než výsledek výše. Původní 934 ms zřejmě těžilo z cache providera. Skutečná cold performance je cca 45 s median.

Závěr: gemma-3-27b je spolehlivý OpenRouter kandidát (17/17, 1413 ms). gpt-oss-120b propadák — reasoning → 7 s a 6× víc out tokenů. Potvrzení teze o thinkingu: qwen3-30b-a3b-instruct 1991 ms vs cloud qwen3.5 (thinking) 10852 ms + out=22671 — past byl režim thinking, ne qwen. Plný záznam: history 2026-06-03 „MiniLoop levné OSS".

gemini-flash-lite — nový rekordman OpenRouter (změřeno 2026-06-03)

google/gemini-3.1-flash-lite na OpenRouteru:

Model Cena Úspěšnost Wall median / avg Tokeny in / out
google/gemini-3.1-flash-lite velmi nízká 17/17 683 / 719 ms 23285 / 559

Nejlepší výsledek ze všech dosud měřených modelů — 683 ms median, 17/17, out pouze 559 tok (přímý parse bez reasoningu). Poráží glm-5.1-ollama (1690 ms), haiku-4.5 (1064 ms) i gemma4:e4b local (1136 ms). Plný záznam: history 2026-06-03 „MiniLoop gemini-flash-lite a mistral-small-3.2 cache".

Malé ollama modely — ministral-3, nemotron-3-nano (změřeno 2026-06-03)

:cloud varianty registrované na nvidia.hell přes POST /api/pull (cloud pointer, žádný GB download):

Model Úspěšnost Wall median / avg out tok
ministral-3:8b-cloud 15/17 1043 / 1169 ms 653
nemotron-3-nano:30b-cloud 16/17 2015 / 2277 ms 7805

Ani jeden nepřekonal mistral-small-3.2 — oba zavrženy. ministral-3-8b udělal skutečnou chybu dne v týdnu (každý pátek → cron * * 6 sobota místo * * 5) — u připomínek vážné, na 8b je to znát; přitom není ani rychlejší než mistral-small (1043 vs 934 ms). nemotron-3-nano-30breasoning sklony (out=7805 tok, ~12× víc než mistral), je 2× pomalejší a jeho jediný FAIL byl rozsekání 1,3,5 na tři cron výrazy (rozvrh ekvivalentní, formát ne). Závěr: pod ~24b instruct (mistral-small, gemma-3-27b) klesá spolehlivost cronu a malé „nano" modely buď chybují, nebo zbytečně reasonují. Plný záznam: history 2026-06-03 „MiniLoop ministral/nemotron-nano".

Lokální gemma4:e4b (změřeno 2026-06-03)

gemma4:e4b (8B, 8 GB) — model stažený přímo na nvidia.hell, žádný cloud, žádné náklady:

Model Úspěšnost Wall median / avg out tok
gemma4:e4b (lokální) 17/17 1136 / 1719 ms 1530

Překvapivě dobré výsledky pro lokální 8B model. Median 1136 ms je rychlejší než glm-5.1 cloud (1690 ms) a blízko gemma-3-27b-it na OpenRouteru (1413 ms). Vysoký avg (1719 ms) oproti mediánu (1136 ms) = odlehlé hodnoty u složitějších vstupů (random/multiple times, 34 s). Žádné skutečné chyby, žádný reasoning. out=1530 tok je 2,5× více než mistral-small (610), ale bez reasoningu — model prostě verbosněji okomentuje. Nejlepší dosud změřený čistě lokální model. Plný záznam: history 2026-06-03 „MiniLoop gemma4:e4b local".

Zamítnuté lokální modely

Model Důvod zamítnutí Median Úspěšnost
phi4:latest (14.7B) 2 skutečné chyby: příští pondělí06-05 (čtvrtek!), dopoledne09:00 místo 08:00. Navíc pomalejší než gemma4:e4b 1744 / 1676 ms 15/17
codestral:22b Pomalý (2483 ms) + skutečná chyba data: příští pondělí06-07 (neděle) místo 06-08 2483 / 2621 ms 16/17
ministral-3:8b-cloud Skutečná chyba weekday v cronu (pátek → cron * * 6 = sobota) 1043 / 1169 ms 15/17
nemotron-3-nano:30b-cloud Reasoning sklon (out=7805 tok), 2× pomalejší než mistral-small 2015 / 2277 ms 16/17

Rychlost: glm-5.1 vs minimax-m3 (Ollama nativní streaming, 2026-06-07)

Měřeno přímo proti Ollamě na nvidia.hell (stejný endpoint jako nanobot), streaming /api/chat, identický /remind list prompt, 3 běhy/model. :cloud modely nevracejí sub-durations (eval_duration ap. = None) — tok/s nutno měřit přes streaming (TTFT = čas 1. content chunku).

Model TTFT (medián) Total wall (medián) Out tok End-to-end průtok (out/total)
glm-5.1 ~5,9 s ~7,5 s 12001730 ~198 tok/s
minimax-m3 ~6,8 s ~10,8 s 420460 ~40 tok/s

minimax-m3 je výrazně línější: TTFT mají srovnatelný (start není problém), ale minimax má ~50 % delší celkovou dobu i přes 34× MÉNĚ vygenerovaných tokenů. Čistá generace minimaxu ~95120 tok/s (streamuje plynule); glm ~5× vyšší end-to-end průtok. Pozn.: glm „1300 tok/s" z post-TTFT okna NEbrat doslovně — cloud buffer flushne dávku, proto měřit out/total. Na interaktivní úkoly je glm-5.1 jednoznačně svižnější. Plný záznam + per-run čísla: history 2026-06-07 „Měření rychlosti glm-5.1 vs minimax-m3".

Širší rozhodovací rozbor (GLM-5.1 vs MiniMax M3 vs Kimi K2.6 — kdy který za podmínky Ollama Cloud, capability cliffs, use-case mřížka): models.md.

Doplněk: minimax-m2.7 vs glm-5.1 (2026-06-07, prokládaně 5 kol)

minimax-m2.7:cloud zmizel z /api/tags (Ollama Cloud ho nahradila m3), ale POST /api/pull ho dotáhne (cloud pointer). Mediány (cloud byl vytížený → absolutní čísla vyšší než ranní m3 měření, ber jen poměr):

Model TTFT Total wall Out tok e2e (out/total)
glm-5.1 15,4 s 18,3 s 1558 ~91 tok/s
minimax-m2.7 9,2 s 11,3 s 351 ~28 tok/s

m2.7 má decode ~3× pomalejší než glm (a horší než m3 ~40 tok/s). Nižší wall-clock (11 vs 18 s) je jen díky terseness (~4,5× méně tokenů), ne rychlejším generováním. Pro delší agentní výstupy (tool args, kód) je pomalý decode handicap. Plný záznam: history 2026-06-07 17:51.


minimax-m3 je pro nanobot agenta nepoužitelný (BLOCKED)

Verdikt: nenasazovat minimax-m3 jako agent model. Vedle pomalosti (~40 tok/s end-to-end, viz sekce výše) má fatální slabinu v agentní recovery — neumí přečíst chybovou hlášku toolu a vystoupit ze smyčky.

Konkrétně (detach deep-research ollama-cloud-models-research, 2026-06-07): web_fetch velké stránky se perzistoval do souboru, parsování přes exec blokoval restrictToWorkspace guard, a minimax-m3 místo aby přesunul soubor / použil read_file (guard to doslova radil) opakoval identický blokovaný příkaz s kosmetickými obměnami, prokládal ho triviálními print('ok') sanity-checky (četl failure jako rozbitý interpreter) a jednou vystřelil 10× tentýž grep v jednom tahu → spálil všech 200 maxToolIterations bez výsledku. Stejný úkol s kimi doběhl za ~456 s.

K tomu už dřív známé: tool-result bug + výrazná pomalost. Zkouší se náhrada minimax-m2.7 (starší MiniMax). Pro background deep-research drž GLM-5.1 / Kimi, ne MiniMax. Plný rozbor smyčky: session detach_2026-06-07T170344-ollama-cloud-models-research.jsonl.

/remind: model nevypisuje celou tabulku → mezera v instrukcích, ne bug skriptu

Problém → příčina → fix: Modely (kimi27 i default) po /remind list dostanou kompletní tabulku (CLI exit 0), ale jeden ji scvrkne na počet, druhý si ji přerenderuje po svém. → Příčina: SKILL.md sekce ## Behavioral contract popisovala formát výstupu, ale neříkala „předej kompletní" — modely to čtou jako surová data k vlastnímu formátování. → Fix: odstavec Showing read results (list/upcoming/delivered = user-ready text, vypsat každou položku s #display-id, nesumarizovat). Plný záznam: history 2026-06-15 13:21.

Diagnostika chování nanobot agenta = číst webui session logy. Konverzace (user/assistant/tool turny, včetně reasoning_content a exec výstupů) žijí na serveru ve workspace/sessions/websocket_<id>.jsonl (kopie i v ~/.nanobot/webui/). Pro „proč model udělal X" stáhnout příslušnou session a číst turny — odhalí, že příkaz uspěl a chyba je až v prezentaci.

Session logy: 89 % objemu jsou tool výsledky

Z 534 souborů v ~/.nanobot/workspace/sessions/ je 332 reálných konverzací (12 311 zpráv, 14,1 MB obsahu). 12,5 MB (89 %) tvoří návratové hodnoty toolů — už oříznuté na maxToolResultChars: 16000. Pro analýzu chování stačí nahradit je metadaty (name(args) → ok|ERROR, velikost), čímž korpus spadne na ~3 MB. Přírůstek je 4,4 session/den, aktivita jen 70 % dní. Zdroj: měření 2026-09-01, skills/reflect/scripts/reflect_distill.py.

reflect: nálezy platí pro okno, ne pro celou historii (2026-09-02)

Dokud běh dohání backlog chronologicky, nálezy popisují nejstarší nezpracované session, i když je předkládá jako aktuální. Reálně: 3 běhy zpracovaly 56 z 356 session, všechny z 26.29. 5., a všech 8 rozhodnutých nálezů (4 aplikované do SOUL.md) tak opravovalo chování z konce května. Fix: since = max(cursor, now - window) — cursor je podlaha (nic dvakrát, jinak merge_findings sečte počty znovu), okno strop (starší session se přeskočí natrvalo). Default --window-days 21. Plný záznam: history.md 2026-09-02.

Dávkování samo nálezy nezkreslovalo — každý z těch tří běhů byl jedna dávka.

reflect: destilát session vyrostl z ~5 kB na ~14 kB (měřeno 2026-09-02)

21denní okno = 74 session = 6 dávek po 200 kB (~1 MB destilátu). Původní plán počítal s ~4,4 session/den po ~5 kB, tedy jednou dávkou. Důsledek: --deadline-minutes 20 + TIMEOUT_SECONDS 45 min zvládnou 12 dávky za noc, takže okno se dohání ~2,4 dne za noc. Při dimenzování dávkovaného běhu nad session logy je tedy nutné velikost měřit (reflect_distill.py --stats), ne odvozovat z počtu session.

reflect: počty v nálezu jsou self-report modelu, ne měření

occurrences / sessions_affected si vymýšlí analyzující LLM a merge_findings je jen sčítá napříč běhy. Nekoherenci to propustilo do store: nález f09a7 měl occurrences: 4 a sessions_affected: 5. Skript umí zkontrolovat jen invarianty (sessions_affected ≤ min(occurrences, počet session v dávce), clamp ve validátoru) — absolutní hodnota zůstává nedokázaná a v review se nesmí prezentovat jako měření.

reflect: dávkovaná analýza nevidí rozptýlené vzory

Prompt zakazoval hlásit jednorázový slip, takže vzor s frekvencí ~1× na dávku se nikdy nepojmenoval → nikdy nespočítal → práh ≥2 výskyty a ≥2 session nepřelezl. Práh sám skládání napříč dávkami zvládá; chyběl mu vstup. Fix: prompt smí hlásit jediný výskyt vzoru, který už je v „Known patterns", a dostal informaci, že vidí jen výsek historie. Novost vzoru se tím nemění — genuinely nový singleton se pořád nehlásí.

reflect: zápis do auditního store patří výhradně skriptu

SKILL.md říkal agentovi, aby u nálezu bez patche „propose the exact old_text/new_text yourself", ale reflect_apply.py patch přijmout neuměl (--new-text-file jen přepíše new_text už existujícího patche). Agentovi nezbylo než editovat findings.jsonl ad-hoc skriptem — session 7a988478 na to spotřebovala 3 jednorázové skripty. Fix (2026-09-02): akce --set-patch <json>, která kandidáta nejdřív prožene check_patch() a teprve pak zapíše, plus věta ve STOP gate 4, že se store needituje ručně nikdy. Poučení obecně: dokud pro nějaký legitimní krok neexistuje volání skriptu, prompt ho nezakáže — agent si cestu najde a bude mít pravdu.

reflect: „naposledy" u nálezu byl dřív datum přepsání záznamu

_seen_line() vydávalo created (kdy se záznam naposledy složil) za „last seen", takže nález refilovaný každou noc hlásil dnešek bez ohledu na stáří důkazů. Ze stejného zdroje plynuly falešné regrese: regression_of se nastavilo, kdykoli se vzor po applied objevil, i když analyzovaná session byla z doby před opravou (retry-without-diagnosis 67× označen REGRESE, nejnovější důkaz 31. 8., patch 1. 9.). Fix (2026-09-02): derivované pole last_seen = max(evidence[].when) (jen tvary ^\d{4}-\d{2}-\d{2}, prvních 10 znaků, fallback created) — a regression_of jen když last_seen > applied.at[:10], jinak watch. when je volný string od modelu a formáty se míchají (2026-08-31 i 2026-08-31 13:53).

reflect: report počítal nálezy po dávkách, ne po vzorech

_run() dělalo merged += batch_findings, takže vzor nalezený v 5 dávkách byl v merged 5× — včetně mezistavů, které fold zahodil. Z toho žil report, stats i Telegram: report z 2. 9. měl 29 nadpisů proti 13 vzorům ve store. Fix (2026-09-02): merged je dict[str, Finding] klíčovaný pattern, poslední zápis (nejvíc složený) vyhrává. Store byl správně po celou dobu — nafouknutá byla jen prezentace.

Vyřešené chyby ve skillu reflect (2026-09-02) — noční běh nedobíhal

Noční cron padal na 30min timeout a zahazoval i to, co už měl hotové:

  • All-or-nothing zápis_write_findings, kurzor i report se dělaly po poslední dávce, takže timeout zahodil dvě dávky s platnými nálezy a kurzor nechal na místě → další noc totéž plus nové session → fix: commit() po každé dávce, kurzor se nikdy nepohne dozadu.
  • Žádný strop na délku běhu → 412 session za kurzorem = 7 dávek ≈ 90 min proti 30 min → fix: soft deadline --deadline-minutes (default 20), TIMEOUT_SECONDS 45 min už jen jako brzda na zaseknutou dávku.
  • 185k-tokenový prompt přetékal provider timeout (_OPENAI_COMPAT_REQUEST_TIMEOUT_S = 120 s, NANOBOT_LLM_TIMEOUT_S = 300 s), retry zahodí hotový prefill a začne znovu → 9 timeoutů za běh → fix: os.environ.setdefault na 600/900 s + dávka 500 kB → 200 kB (~70 k tokenů).
  • Infra chyba šla do JSON validátoru → nanobot vrací Error calling LLM: … jako odpověď agenta, validátor to hlásil jako „invalid JSON" a spotřeboval 1 ze 3 pokusů, model dostal vytýkáno něco, co nenapsal → fix: RunResult.stop_reason == "error" se pozná dřív, přepošle se původní prompt v čerstvé session, vlastní strop 2 pokusy.

Efekt: dávka z ~13 min na 147 s, jedna iterace, žádný retry. Plný záznam: history.md 2026-09-02.

RunResult z Nanobot.run() rozlišuje selhání providera

nanobot/sdk/types.py:50RunResultstop_reason a error. Když LLM call selže, nanobot vrátí text chyby jako obsah odpovědi (Error calling LLM: timed out after 300s) a zároveň nastaví stop_reason="error" + error (agent/runner.py:289-305, agent/loop.py:1019). Skript, který parsuje odpověď agenta, musí tenhle stav testovat před validací — jinak diagnostikuje infra výpadek jako vadný výstup modelu.

Timeouty LLM callu v nanobotu: 120 s na request, 300 s na celý call

Dvě vrstvy, obě přebitelné env proměnnou:

Vrstva Default Env Zdroj
jeden HTTP request na OpenAI-kompatibilní endpoint 120 s NANOBOT_OPENAI_COMPAT_TIMEOUT_S providers/openai_compat_provider.py:89,163
celý call včetně retry (3 pokusy) 300 s (0 = vypnuto) NANOBOT_LLM_TIMEOUT_S agent/runner.py:702
idle mezi chunky ve streamu 90 s NANOBOT_STREAM_IDLE_TIMEOUT_S providers/base.py:20

SDK cesta (Nanobot.run()) nestreamuje (stream: False), takže idle timeout se jí netýká. Retry počítá prefill znovu od nuly — u desítek tisíc tokenů promptu je tedy dražší než čekání, proto se u dávkových skriptů vyplatí request timeout zvednout, ne zkracovat.

reflect: --new-text-file bez --check aplikuje okamžitě (2026-09-02)

reflect_apply.py --new-text-file <path> není náhled — bez --check jde přímo do apply_finding(), tedy zápis souboru + commit. Náhled uživatelovy editace je až --check --new-text-file, a protože se jeho text do patch neukládá, musí temp soubor přežít do aplikace a předat se znovu. SKILL.md to mělo v tabulce rozhodnutí zaměněné (řádek edit: sliboval diff, předepisoval aplikaci) — opraveno, viz history 2026-09-02 14:55.

Vyřešené chyby ve skillu reflect (2026-09-01)

Čtyři tiché chyby — nic nespadlo, jen se dělo něco jiného, než co slibovala dokumentace:

  • Kurzor nefiltroval_run ukládal zformátovaný stamp (2026-07-11 14:02), collect_sessions ho porovnávala proti syrovému ISO (…T09:00:…); 'T' > ' ', takže 9 session z dne kurzoru se analyzovalo znovu každý běh a výskyty se počítaly dvakrát → fix: SessionDigest.started drží syrové ISO, formátuje se až v hlavičce.
  • Zamítnutí vydrželo jen jeden běhmerge_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 2026-09-01.

Test může projít kolem chyby, když si vstup vyrobí v jiném formátu než volající

test_since_excludes_already_processed_sessions podával since jako "2026-08-01T00:00:00" — formát, který produkční kód nikdy nevyrobí (ukládal "2026-08-01 00:00"). Test byl zelený a chyba běžela v produkci. U hodnot, které jedna část kódu zapisuje a druhá čte, testuj round-trip, ne literál. Zdroj: skills/reflect/tests/test_reflect_distill.py, history 2026-09-01.

Tokenizace nanobot korpusu: ~1,2 znaku na token

Naměřeno na reálném destilátu (90 kB promptu → 75 135 tokenů podle tiktoken v nanobot.agent.memory). Čeština, názvy toolů a UUID se tokenizují špatně. Odhad „34 znaky na token" je pro tenhle korpus 3× mimo — kdo počítá velikost dávky, musí použít 1,2.

contextWindowTokens presetu přebíjí agents.defaults

agents.defaults.contextWindowTokens je 65536, ale preset glm 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 glm.

Velká zpráva projde, snip_history krátí jen historii

ContextGovernor.snip_history (agent/context_governance.py:383) zahazuje celé starší zprávy, nikdy nekrátí jednu zprávu — a nejnovější zprávu přidá vždy, i když sama překročí budget (if kept and kept_tokens + msg_tokens > remaining_budget: break, řádek 422). Velký vstup je proto lepší poslat přímo ve zprávě než souborem přes read_file, který by ho uřízl na maxToolResultChars (16 000 znaků).

uv není v PATH neinteraktivního SSH

ssh nanobot@nanobot.hell 'uv run …' skončí failed to run command 'uv': No such file or directory. Je v /home/nanobot/.local/bin/uv — přes SSH je nutná plná cesta. Crontab si PATH nastavuje sám (PATH=/home/nanobot/.local/bin:/usr/bin:/bin), tam bare uv run funguje. Souvisí s tools.exec.pathPrepend (history 2026-08-01).

LLM rozbíjí JSON českou uvozovkou — a hláška o tom musí být konkrétní

GLM-5.3 při psaní české diagnózy do JSON stringu napsal („repeated external lookup blocked"): otevírací uvozovka je (U+201E), ale zavírací je ASCII ", která neescapovaná ukončí string. Generická hláška „no parseable json block" nedá retry nic použitelného a všechny 3 pokusy selžou stejně. Fix: validátor hlásí msg, řádek, sloupec a výřez textu okolo error.pos, plus prompt zakazuje uvozovky uvnitř string hodnot. Po opravě prošel první pokus. Zdroj: skills/reflect/scripts/reflect_auto.py:_decode_payload, history 2026-09-01.

~/.nanobot/workspace je git repo a nanobot do něj commituje sám

Lokální repo bez remote; 15 z posledních 20 commitů je nanobot <nanobot@dream> (Dream dělá dream: periodic memory consolidation). .gitignore vynechává db/ sessions/ log/ tmp/ backup/ tasks/ cron/runs/. Důsledek: skill, který mění soubory na serveru, nepotřebuje vlastní zálohy — commit před změnou a git revert stačí. Pozor: není to záloha mimo stroj a je to repo nespojené s src/nanobot.

Stav skillu nesmí bydlet v adresáři skillu

rsync -av skills/<name>/ … přepisuje celý adresář, takže state.json nebo databáze uvnitř skills/<name>/ se při nasazení ztratí. Data patří do vlastního adresáře v rootu workspace — vzor skills/note/ + notes/, nově skills/reflect/ + reflect/.

reflect: odhad šance opravy stojí na tom, jestli je text v kontextu (2026-09-02)

/reflect ukazuje u nálezu odhad ~80 / ~60 / ~40 / ~20 %, že oprava vzor skutečně zastaví. Hlavní osa rubriky není kvalita formulace, ale jestli je opravovaný text v kontextu ve chvíli, kdy chyba vzniká: gate ve skriptu drží vždy (~80 %), tvrdý zákaz v SOUL.md/AGENTS.md/SKILL.md dotčeného skillu ~60 %, přeformulování tamtéž ~40 %, soubor mimo kontext nebo ponechání na úvaze agenta ~20 %. regression_of sráží o pásmo — instrukce toho druhu už na tom vzoru jednou selhala. Pásma, ne přesná čísla: je to odhad ze záznamu, ne měření. Zdroj: skills/reflect/SKILL.md sekce „Estimating the odds", history 2026-09-02.

reflect: --workspace umožní zkoušet zásahy mimo ostrý store (2026-09-02)

reflect_apply.py --workspace <cesta> bere kompletní workspace odjinud, takže se dá --set-patch/--check/--apply vyzkoušet na kopii (findings.jsonl + cílový soubor v tmp/) a ostrý store i cílový soubor zůstanou nedotčené. Použito při ověření draftování patche na reálném nálezu f5c34. Zdroj: skills/reflect/scripts/reflect_apply.py:250, history 2026-09-02.

keep.md je v kontextu každého tahu — proto je jeho čistota load-bearing (2026-09-04)

AGENTS.md## Explicit user details říká „Explicit user facts are stored in keep.md. Read at every turn". Co v keep.md leží, platí model každý tah. Dva praktické důsledky: pravidlo pro agenta uložené v keep se čte jako fakt o uživateli, ne jako instrukce (slabší tah než AGENTS.md), a referenční materiál tam obchází /note, kde ho uživatel hledá přes /note search. Do keep patří jen trvalý fakt / preference / rozhodnutí o uživateli; ostatní routuje ## Triage v skills/keep/SKILL.md. Zdroj: history 2026-09-04.

notes/ v serverovém workspace repu commituje výhradně skill note (2026-09-04)

skills/note/SKILL.mdVersioning: Dream se notes/ nedotýká, takže note je jediný, kdo tam commituje — a vždy jen git add notes/, nikdy git add -A (zbytek workspace vlastní Dream). Zbytek serverového workspace repa běžně stojí s necommitnutými změnami (keep.md, AGENTS.md, cron/jobs.json, reflect/*) — to je normální stav, ne rozbité repo. Při ruční editaci notes.md proto commitni s prefixem note: a stage jen notes/. Zdroj: history 2026-09-04.

Ollama Cloud embeddingy neexistují — cloud tier je completion-only (2026-09-09)

Žádný z 18 cloud modelů nemá capability embedding; /api/embed na cloud modelu vrací unauthorized, zatímco /api/generate na tomtéž modelu projde — takže to není problém autorizace, cloud tier ten endpoint prostě neobsluhuje a hláška je zavádějící. Katalog na ollama.com to potvrzuje: filtr c=embedding vrací 12 modelů, všechny jen ke stažení, žádný s cloud tagem. Embeddingy je tedy nutné hostovat lokálně. Zdroj: history 2026-09-09.

Cena brute-force cosine v SQLite je z 96 % Python režie, ne matmul (2026-09-09)

Nad 10⁵×1024 float32: 594 ms = read 382 + pack 190 + dot 22. Samotný numpy matmul je zanedbatelný; platí se za extrakci BLOBů a jejich složení do matice. Proto sqlite-vec (vec0) dá 19 ms @ 10k a 212 ms @ 100k — skenuje v C. .npy + mmap_mode='r' je ještě rychlejší (35 ms), ale za cenu druhého souboru mimo DB. Pozor: vec0 není ANN index — lineární škálování 19 → 212 ms to potvrzuje, výhoda je konstanta, ne asymptotika. Zdroj: history 2026-09-09.

FTS5 a čeština: remove_diacritics + prefix wildcard stačí, trigram netřeba (2026-09-09)

unicode61 remove_diacritics 1 a výš foldí diakritiku (dotaz zaloha najde záloha; hodnota 0 ne). Stemming unicode61 neumí, ale prefix wildcard to pokryje: záloh* i zaloh* najdou záloha, zálohování, zálohy. Dotazová vrstva tedy lepí * na termy — trigram tokenizer není potřeba. Souvisí s varováním v remind/scripts/store.py (find_active_by_exact_text()), že SQLite lower() foldí jen ASCII. Zdroj: history 2026-09-09.

vec0 není cílem foreign key — kaskáda ho nevyčistí (2026-09-09)

ON DELETE CASCADE z nadřazené tabulky uklidí navázané řádky i FTS5 external-content tabulku (AFTER DELETE trigger se na kaskádě spustí), ale řádek ve vec0 virtuální tabulce osiří — indexer ho musí mazat explicitně. Jinak index při mazání souborů tiše plní mrtvými vektory. Ověřeno smoke testem; DELETE/re-INSERT/UPDATE po rowid a ROLLBACK ve vec0 jinak fungují normálně (sqlite-vec v0.1.6). Zdroj: history 2026-09-09.

Ollama keep_alive musí být číslo, ne string (2026-09-09)

{"keep_alive": -1} i {"keep_alive": "24h"} → HTTP 200; {"keep_alive": "-1"} → HTTP 400. Cold load qwen3-embedding:0.6b (639 MB) je 1,81 s vs. warm 0,043 s, takže připíchnutí modelu se vyplatí, ale není kritické. Batching je naopak podstatný: 8,9 chunk/s po jednom vs. 108 chunk/s v batchi 32. Zdroj: history 2026-09-09.

Instruct prefix je u qwen3-embedding load-bearing, ne kosmetika (2026-09-09)

Na 12 parafrázových dotazech nad 123 reálnými chunky: recall@10 8/12 s prefixem vs. 3/12 bez něj (MRR 0,261 vs 0,204). Plán final-wiki-hybrid-rag ho označoval za „nekritickou vlastnost" na základě 5/5 vs 5/5 na jiném korpusu — na uživatelských datech to neplatí. Prefix musí být bitově identický při indexaci i dotazu, proto žije v meta.query_prefix a jeho změna vyžaduje wiki_sync.py --full. Zdroj: history 2026-09-09.

FTS5 prefix wildcard NEfolduje českou flexi — jen prefixy (2026-09-09)

záloh* najde záloha/zálohování/zálohy jen proto, že záloh je kmen. Se skutečně skloněným dotazovým slovem to selže: naměřeno cestu*0 chunků v travel/packaging-list.md, cesty* → 3, cest* → 4. Wildcard je prefixový, takže pomůže jen když je dotazové slovo prefixem tvaru v dokumentu — a česká flexe mění koncovku, ne začátek. Důsledek: dotaz „co si vzít na cestu do zahraničí" nedostal doslovný seznam věcí na cestu ani do top 10. Tohle je polovičnost D6 v plánu, ne chyba implementace. Zdroj: history 2026-09-09.

Hybrid RRF zlepšuje rank, ne pokrytí (2026-09-09)

Nad reálnými poznámkami: MRR 0,367 u RRF vs 0,257 (BM25 sám) a 0,261 (vektory samy) — merge dvou ranků téže množiny opravdu vyhrává. Ale recall@10 u RRF je 6/12, zatímco vektory samotné 8/12: RRF řadí podle shody obou polovin, takže chunk, který našla jen vektorová polovina na ranku #9, vytlačí z top-10 chunky, na kterých se poloviny shodnou. Očekávaný kompromis, ne vada. Zdroj: history 2026-09-09.

Bare python3 neotevře vec_chunks — potřebuje sqlite_vec.load() (2026-09-09)

Dotaz na běžné tabulky (chunks, files) přes systémový python3 projde, ale jakmile se sáhne na vec0 virtuální tabulku, přijde sqlite3.OperationalError: no such module: vec0. Extension se musí načíst explicitně (enable_load_extension(True) + sqlite_vec.load(conn)), což skill dělá v wiki_db.get_db(). Při ruční inspekci indexu na serveru je proto nutné uv run --with "sqlite-vec==0.1.6". Zdroj: history 2026-09-09.

ty.toml extra-paths je plochý jmenný prostor — kolize modulů mezi skilly (2026-09-09)

Typechecker ty řeší import store proti seznamu extra-paths a vyhrává první cesta, takže dva skilly se stejným jménem modulu si navzájem rozbijí kontrolu (testy wiki dostávaly store z remind). Proto všechny skilly kromě remind prefixují moduly jménem skillu (note_capture.py, wiki_sync.py) — je to nutnost, ne estetika. U kolize, které se nelze vyhnout (wiki_search.py je i v retired llm-wiki), rozhoduje pořadí v extra-paths. Zdroj: history 2026-09-09.

Ollama usage API: co v odpovědi je a co ne

GET https://ollama.com/api/usage (Bearer klíč z workspace/.env) vrací limits.session (hodinové okno) a limits.weekly, každé s usage (zlomek limitu) a models: [{name, request_count}]pole, ne slovník. activity.cost je na Pro plánu rozbité (vždy 0.00000). Žádné reset timestampy, žádné tokeny, žádný cost split.

usage má 3 desetinná místa (0.072, 0.156) → rozlišení 0,1 % limitu. Delta za minutu je proto skoro vždy 0 — jediná přesná veličina je request_count per model. Ověřeno 2026-09-15.

Nanobot nemá nikde per-session spotřebu tokenů

  • memory/history.jsonl = destilát paměti z Dream procesoru (cursor, timestamp, content, u části session_key) — není to účtovací log, žádné tokeny.
  • sessions/*.jsonl = per-turn zprávy (role, content, timestamp, tool_calls, reasoning_content, latency_ms) — taky bez tokenů a bez názvu modelu.
  • Jediný zdroj tokenů je LLM usage: prompt=… completion=… cached=… v journalu, což je DEBUG (vyžaduje -v v ExecStart, viz sekce o log levelu) a navíc nenese session id.

→ Atribuce spotřeby na session jde jen časovou korelací (timestampy v sessions/*.jsonl a Processing message from … v journalu) proti řadě vzorků z db/ollama_usage.sqlite. Ověřeno 2026-09-15, zdroj: plans/ollama-usage-poller.md sekce Revize.


Ollama session okno: 5 h kotvených prvním requestem, ne pevná mřížka

Okno nezačíná na pevné mřížce — začíná prvním requestem po vypršení předchozího (jako 5h sessions v Claude Code). Naměřeno 2026-09-15: usage zůstala na 0.077/21 req přes 05:00 UTC (pevná mřížka by ji tam vynulovala a poller by to zapsal) a resetovala se až s requestem v 06:00, po 93minutové pauze. Rekonstrukce aktivity z sessions/*.jsonl dává souvislý řetězec 00:0005:00, pak 06:0011:00 UTC.

Délka 5 h je z primárního zdroje transparent-pricing („no 5-hour or weekly limits" je to, co nové plány zrušily; tenhle klíč je pořád má). Že jde o okno a ne rolling counter je taky měřené — pokles 0.077/21 → 0.0/{} naráz, rolling by se rozpadal postupně.

Praktický důsledek: konec okna = poslední pozorovaný přechod v samples

  • 5 h. Za konec se nesmí extrapolovat — další okno začne až dalším requestem, takže čas neexistuje, dokud request nepřijde. Detail: history 2026-09-15 10:15.

Pozor na past: crony nanobota běží na celé hodiny (Dream po 2 h atd.), takže první request po pauze často padne na celou hodinu. Hranice pak vypadají jako pevná mřížka, i když nejsou.

Zamítnuto: kalibrace fáze průnikem kandidátů modulo 300 min. Stála na předpokladu pevné mřížky a navíc potřebovala víc pozorování, než začala hlásit cokoli. Nahrazeno výše. Vedlejší poznatek, který z ní zbyl: 5 h nedělí 24 h, takže „mřížka 00/05/10/15/20 UTC" nemůže být stálá tak jako tak.

limits.session.usage == procento na dashboardu; countdown na webu ne

API 2.3 % sedělo přesně s tím, co uživatel viděl na webu. Countdown na webu je ale ke kalibraci nepoužitelný — zaokrouhluje neznámo jak: v 06:58 UTC ukazoval „4 hodiny" a v 07:05 „3 hodiny". Proto skill tiskne i konkrétní čas, ne jen „resets in X".

Zamítnuto: heartbeat v usage polleru

Nápad zapisovat vzorek i beze změny (aby mezera v samples znamenala výpadek) je zbytečný — interval (poslední před, první po] kolem resetu je pravdivý bez ohledu na to, jestli poller běžel; výpadek ho jen rozšíří. Uživatel ho zamítl a měl pravdu: DB zůstává čistě změnová.

uv není v PATH při neinteraktivním SSH

ssh nanobot@nanobot.hell 'uv run …' selže na uv: command not found — je v ~/.local/bin, který se nenačte. Crontab to řeší vlastním řádkem PATH=/home/nanobot/.local/bin:/usr/bin:/bin. Při ručním spuštění přes SSH předřadit export PATH=/home/nanobot/.local/bin:$PATH.