runtime data

This commit is contained in:
lachtan
2026-07-22 12:32:23 +02:00
parent 8e66d6b92a
commit 94dccb3758
11 changed files with 1475 additions and 194 deletions

View File

@@ -4,6 +4,12 @@ Ověřená fakta o vnitřním fungování nanobota. Stručně, s případným od
---
## `uv` na serveru není v non-interaktivním SSH PATH
`ssh nanobot@nanobot.hell "uv run ..."` selže s `uv: command not found` — non-login shell nemá `~/.local/bin` v PATH. Plná cesta je `/home/nanobot/.local/bin/uv`. Cron skilly to obcházejí shebangem `#!/usr/bin/env -S uv run --script`. Zdroj: nasazení remind `upcoming` 2026-06-10 (history.md).
---
## Kdy je a není potřeba restart nanobot.service
**Restart NENÍ potřeba:**
@@ -49,6 +55,7 @@ Naplánované joby jsou v `/home/nanobot/.nanobot/workspace/cron/jobs.json`. Str
Změna se projeví **bez restartu** — cron service volá `_load_store()` při každém ticku (`nanobot/cron/service.py:394`), jobs.json se čte čerstvě. Hot reload tedy funguje out-of-box.
**Editace:** Python in-place editor přes SSH, např.:
```bash
ssh root@nanobot.hell "python3 -c \"
import json; from pathlib import Path
@@ -68,7 +75,7 @@ Nanobot cron job **vždy** běží přes agenta (`agent.process_direct`) — nea
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).
**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".
@@ -174,23 +181,29 @@ Zdroj: `nanobot/channels/telegram.py:258-326` (BotCommand registrace, regex rout
**Telegram:** `/remind text` je bot command, nedojde k agentovi jako text. Psát přirozeně: `připomeň mi...`, `nastav připomínku...`
**Read-back doručených („co dnes přišlo?") = subcommand `delivered`**, ne čtení logu. `remind_edit.py delivered [--since YYYY-MM-DD]` dotáhne z `reminder_fires` jen doručené (`status='delivered'`), default dnes; `fire_time`/`delivered_at` se ukládají **Prague-naive**, takže žádná konverze. `TOOLS.md` na to směruje agenta.
**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_edit.py`** (deterministické CLI, SQLite transakce + validace cron/`at`/random), nikdy přímý DB nebo `edit_file`. Volat `uv run skills/remind/scripts/remind_edit.py <subcommand>` (workspace-relativní cesta, exec běží z workspace rootu). Subcommandy: `list`, `add --text … (--cron EXPR… | --at ISO… | --random-times-per-day N --random-window HH:MM-HH:MM [--random-days 1-5] [--random-from DATE] [--random-until DATE])`, `edit --keyword|--id [--text …] [--replace-schedules + nové schedule flagy]`, `remove`, `enable`, `disable`, `delivered`. Výběr záznamu přes `--keyword` (substring; ambiguous → vrátí ids) nebo `--id` (přesný). **Mutace vrací JSON** (`{"added": …}` ap.); **`list` vrací čitelný text** — řádek na reminder `#<id> text [enabled|disabled]` + odsazené schedule řádky (prefix `#<id>` je id pro `--id`), prázdný store → `(no active reminders)`. Chyby stderr + non-zero exit. Env `REMIND_DB` přepíše cestu k DB (testy).
**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_edit.py` jde přes stejný `compute_fire_times`. Testy: `uv run --with pytest pytest skills/remind/tests/`. Návrh: [plans/remind-random-time.md](plans/remind-random-time.md).
**Náhodný (deterministický) čas (`random` blok):** N× denně v náhodný čas uvnitř okna, ale deterministicky — sdílený modul `scripts/random_times.py` počítá časy ze seedu `f"{datum}|{text}"`, takže sender zůstává bezstavový (počítá se každou minutu znovu). Min. rozestup mezi časy = konstanta `MIN_GAP_MIN` (default 15) v tomtéž modulu. Validace v `remind_cli.py` jde přes stejný `compute_fire_times`. (`random_times.py` taky drží sdílené `minutes_to_hhmm` — dřív duplikované v cli i send.) Testy: `uv run --with pytest pytest skills/remind/tests/`. Návrh: [plans/remind-random-time.md](plans/remind-random-time.md).
**Týdenní náhodný režim (`period='week'`, od 2026-06-15):** `--random-times-per-week N` (mutually exclusive s `--random-times-per-day`) rozprostře N odpálení přes týden PoNe na N **různých** náhodných dnů, 1 čas na den uvnitř okna. Engine zůstává **per-day volaný**`_weekly_fire_times` spočítá celý týdenní plán deterministicky ze seedu `f"{pondělí}|{text}|week"` (ne ze dne) a vrátí jen fires daného dne, takže daemon i forecast nemění iterační logiku. Sloupec `schedule_random.period` ('day'/'week', default 'day') + idempotentní `_migrate(conn)` v `db.get_db` (PRAGMA-guarded `ALTER TABLE ADD COLUMN`, protože `init_db` na živou DB nesahá). `count > dostupné dny` se clampuje (hraniční týden u from/until), `count > kapacita filtru` raisuje. Detaily: history 2026-06-15.
**Display IDs (autonomní serverová feature, do repa dotaženo 2026-06-15):** `list`/`upcoming` ukazují **1-based pozici** mezi aktivními remindery (`#1`, `#2`…, počítáno on-the-fly v `_active_display_order`), **ne** interní DB id. `--id` i chybové hlášky (`{"error":"no match","display_id":n}`, `ambiguous` s `display_id`) pracují s display ID. Přečíslovává se po každém `remove` → při nejistotě vždy nejdřív `list`. Interní id se uživateli nikdy neukazuje. (Objeveno při syncu před weekly deployem — feature byla na serveru, ne v repu; pravděpodobně Dream procesor. history 2026-06-15.)
**uv path na serveru:** `/home/nanobot/.local/bin/uv` — není v PATH pro root. Spouštět jako `/home/nanobot/.local/bin/uv run script.py`.
**DB_PATH path (gotcha):** V `remind_send.py`/`remind_edit.py` je `Path(__file__).resolve().parent.parent.parent.parent`**4 levely** nahoru z `.../workspace/skills/remind/scripts/` na workspace root, pak `/db/reminders.sqlite`. Se 3 levely míří cesta mimo (`.../workspace/skills/`) a DB se vytvoří/hledá na špatném místě. Override přes env `REMIND_DB`.
**DB_PATH path (gotcha):** V `remind_send.py`/`remind_cli.py` je `Path(__file__).resolve().parent.parent.parent.parent`**4 levely** nahoru z `.../workspace/skills/remind/scripts/` na workspace root, pak `/db/reminders.sqlite`. Se 3 levely míří cesta mimo (`.../workspace/skills/`) a DB se vytvoří/hledá na špatném místě. Override přes env `REMIND_DB`.
**jobs.json se nepersistuje přes restart agenta:** Změny v `cron/jobs.json` provedené agentem přes `edit_file` tool se mohou ztratit po restartu service (nanobot drží jobs v paměti a přepisuje soubor). Bezpečnější: editovat Python in-place přes SSH + ihned restartovat service.
**Identita reminderu = `id` (SQLite autoincrement).** `text` slouží jako lidský klíč pro `--keyword` (substring match), ale nejednoznačné/duplicitní texty se řeší `--id` (z `list` nebo z `ambiguous` chyby, která ids vypíše). Dedup je per `reminder_id`+`schedule_id`, takže i stejné texty se odpalují nezávisle. (Historie: per-entry ID bylo nad YAML zvažováno a zavrženo — history 2026-06-02; migrace na SQLite ho zavedla nativně.)
**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
@@ -206,6 +219,7 @@ Modely se přidávají jako položky do `modelPresets` v `~/.nanobot/config.json
1. **Ověř dostupnost u providera.** Pro Ollama: `curl http://nvidia.hell:11434/api/tags` a zkontroluj, že název modelu (přesně, včetně `:cloud` suffixu) je v seznamu. Pro OpenRouter: `curl https://openrouter.ai/api/v1/models`.
2. **Edituj config in-place** přes Python (zachová ostatní klíče včetně secrets):
```bash
ssh nanobot@nanobot.hell 'python3 -c "
import json, pathlib
@@ -215,10 +229,13 @@ Modely se přidávají jako položky do `modelPresets` v `~/.nanobot/config.json
p.write_text(json.dumps(c, indent=2))
"'
```
3. **Restartuj službu**, aby gateway preset načetla:
```bash
ssh nanobot@nanobot.hell 'XDG_RUNTIME_DIR=/run/user/1000 systemctl --user restart nanobot.service'
```
4. **V chatu** (Telegram/WebUI) přepneš příkazem `/model <preset-name>`.
**Konvence pojmenování presetů:** `<model-zkratka>-<provider>` (např. `kimi-k2.6-openrouter`, `glm-5.1-ollama`). Suffix providera je důležitý — uživatel chce v názvu vidět, odkud model jede.
@@ -279,7 +296,7 @@ Zdroj: `nanobot/agent/skills/`, `ContextBuilder._load_bootstrap_files()`
## Skill `description` — k čemu reálně slouží (progressive loading)
Pole `description` ve frontmatteru non-always skillu je **routing signál**, ne kontext „jak skill funguje". Při sestavování system promptu se každý non-always skill vykreslí jako **jeden řádek** v seznamu: `- **<name>** — <description> \`cesta/k/SKILL.md\``. Tělo SKILL.md se načte **až on-demand**, když si agent skill sám přečte přes `read_file`. Důsledky:
Pole `description` ve frontmatteru non-always skillu je **routing signál**, ne kontext „jak skill funguje". Při sestavování system promptu se každý non-always skill vykreslí jako **jeden řádek** v seznamu: `- **<name>** — <description> \`cesta/k/SKILL.md\``. Tělo SKILL.md se načte **až on-demand**, když si agent skill sám přečte přes`read_file`. Důsledky:
- `description` je jediná info o skillu v promptu, dokud agent nečte tělo → patří tam jen *kdy/proč* skill spustit (trigger fráze, odlišení od příbuzných skillů), **ne** *jak* funguje.
- `description` se **nezkracuje** (`_get_skill_description` vrací text doslova) a je v promptu **každý tah** u všech skillů → trvalý token cost. Drž stručně, routing-orientovaně. Detailní postup patří do těla.
@@ -379,6 +396,7 @@ už používá `/remind` (viz "Cron job s LLM agentem je nespolehlivý…" výš
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"]`).
@@ -386,6 +404,7 @@ Architektura podobná `/remind` — orchestrace mimo agent loop, žádný cron p
**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)
@@ -433,7 +452,7 @@ Daemon notifikuje **jen Telegram** (přes Bot API, deterministicky). Když task
Když skill volá `exec` bez explicitního `working_dir`, příkaz běží s **CWD = workspace root** (`~/.nanobot/workspace`), **ne** v adresáři skillu. Cesty na skripty skillu proto musí být buď workspace-relativní (`skills/<name>/scripts/x.py`) nebo absolutní — **skill-dir-relativní `scripts/x.py` se rozbije** (resolvuje na `workspace/scripts/x.py`).
Zdroj: upstream `nanobot/agent/tools/shell.py:148` (`working_dir=ctx.workspace`) + `:370` (`cwd = working_dir or workspace_root`). Pozn.: remind SKILL.md používá `scripts/remind_edit.py` — v tomto ohledu zavádějící; detach používá korektní `skills/detach/scripts/…`.
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/…`.
---
@@ -493,6 +512,7 @@ Install: `systemctl --user enable --now <unit>.path`. Lingering musí být zapnu
**Gotcha — level-triggered `.path` + startup crash = permanentní latch:** Když oneshot daemon spadne **ve startup fázi** (před vyprázdněním inboxu), inbox zůstane neprázdný → `.path` ho hned znovu spustí → další pád → … Na manager defaultu (`StartLimitIntervalSec=10s`, `Burst=5`) to za <2 s narazí na rate-limit a systemd zalatchuje **`.service` i `.path`** do `failed (unit-start-limit-hit)`. Z toho se **sám nezotaví** — nutný `systemctl --user reset-failed <unit>.service <unit>.path` + `restart <unit>.path`. (Stalo se 7.6., když daemon padal na `NameError`.)
**Hardening (ověřeno, nasazeno na tasks-daemon):** v `.service` přidat
```ini
[Unit]
StartLimitIntervalSec=1800
@@ -501,6 +521,7 @@ StartLimitBurst=20
Restart=on-failure
RestartSec=60
```
`Restart=on-failure` + `RestartSec` dá **delay mezi pokusy** (nezávisle na `.path` retriggeru); čistý `exit 0` (inbox vyprázdněn) ani SIGTERM od systemd nerestartují. Širší okno (`30min`/`20`) zajistí, že se latch po posunu okna sám pustí dál. **`man systemd.service`: pro `Type=oneshot` jsou zakázané jen `Restart=always`/`on-success`, `on-failure` je povolený.**
Zdroj: `man systemd.path` + `man systemd.service`, ověřeno smoke testem před deployem detach skillu; latch+hardening history 2026-06-07 19:33.
@@ -536,13 +557,19 @@ On-demand skill pro okamžitou explicitní paměť. Uživatel řekne „keep X"
---
## Skill `/note` — explicit notes (no auto-load)
## Skill `/note` — osobní znalostní báze (capture → compile, od 2026-07-01)
On-demand skill pro ukládání poznámek do `workspace/notes.md`. Uživatel řekne „note X" → agent reformuluje na terse fact → zapíše jako bullet. Bez dedup, bez kompakce, bez dat.
Přepsáno z SQLite row-store na **capture → compile pipeline** (vzor llm-wiki/detach, ale lehčí: jeden dokument, žádný index/graph/lint). Plná historie: history 2026-07-01. Plán: [plans/note-prepis.md](plans/note-prepis.md).
**Klíčový rozdíl od `/keep`:** `notes.md` nemá referenci v `USER.md` ani jiném bootstrap souboru — nikdy nevstupuje do context window automaticky. Maže se výhradně přes `/note delete <pattern>` (by index nebo substring).
- **Ú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/`).
**Kde žije:** `workspace/notes.md`. Edituje ho výhradně `/note` skill. Odděleno od `/keep`, Dream, MEMORY.md — žádný cross-read ani cross-write.
## 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).
---
@@ -578,6 +605,7 @@ Nanobot má **hardcoded default `context_window_tokens = 65_536`** pro `ModelPre
Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5 / nemotron-3-super 262144, minimax-m2.7 204800, glm-5.1 196608, deepseek-v4-flash 1048576) + `maxTokens` 16384. **Bez restartu** — `modelPresets` se hot-reloadují (viz sekce „Kdy je a není potřeba restart"). U `:cloud` modelů hostí kontext Ollama cloud, takže `contextWindowTokens` reálně rozšíří budget — není to lokální `num_ctx` žeroucí RAM. Plný záznam: history 2026-06-02.
**Důsledky (trade-off, ne čistá výhra):**
- **+** Méně ořezávání/komprese historie → lepší návaznost v dlouhých sezeních. Delší souvislé odpovědi (16k vs 8k output).
- **** „Lost in the middle": LLM neudrží kvalitu rovnoměrně přes celý kontext; info zahrabané uprostřed ~200k se vybavuje hůř. Propad je výraznější u slabších MoE modelů (glm/qwen/nemotron) než u špičkových (Kimi K2.6). Roste latence i protečené tokeny úměrně naplnění.
- Při běžném (nízkém) naplnění se kvalita **nemění** — efekt nastává až když sezení přeroste 65k.
@@ -597,6 +625,7 @@ Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5
**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
@@ -606,12 +635,28 @@ Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5
Zdroj: `nanobot/agent/tools/sandbox.py`, `Dockerfile` (ověřeno 2026-06-05).
**Korekce (2026-07-02): na produkčním `nanobot.hell` je sandbox VYPNUTÝ, takže pip závislosti přes `uv run` fungují.** Server má v `config.json` `exec` blok `"sandbox": ""` + `pathAppend` na `~/.local/bin/uv`, takže `exec` běží bez bwrap — stejné prostředí jako plain SSH. Skilly reálně jedou se závislostmi (`remind`→croniter, `llm-wiki`→pyyaml) a `bookmark`/`html_to_markdown.py` používá `trafilatura` (v2.1.0 ověřeno naživo: `uv run --with trafilatura` stáhne a naimportuje). Limity výše platí jen pro nasazení se *zapnutým* bwrap (`"sandbox": "bwrap"`), typicky Docker. Zdroj: live test 2026-07-02, viz `history.md` téhož dne.
---
## Rozpad input contextu (co zabírá tokeny každý tah)
Detailní naměřený rozpad ~15k baseline input contextu (system prompt po částech + tool definitions 18 toolů) je v samostatném souboru [`tokens-explain.md`](tokens-explain.md) — k 0.2.1, preset glm-5.1. Stručně: ~8,7k system prompt (největší `MEMORY.md`, `skills_section`, `SOUL.md`), ~5,2k tool defs, zbytek session zprávy.
## llm-wiki: zaseknutí způsobuje LLM v mechanické smyčce, ne pomalost
Při wiki lint/fix operacích se agent zasekl proto, že dělal **mechanickou práci v LLM smyčce** (čte stránku → hledá problém → edituje → další stránka). Správné řešení přesouvá tuto práci do deterministického skriptu — LLM rozhoduje jen tam, kde je skutečně potřeba úsudek.
**Praktické důsledky pro llm-wiki:**
- Lint musí vracet `(soubor, řádek, návrh opravy)` — ne jen "9 broken refs" → agent nemusí číst žádnou stránku
- `--fix` mód má smysl jen pro mechanické triviality (chybějící frontmatter pole ze šablony) kde není úsudek
- Slug collision a frontmatter šablona patří do compile pipeline, ne do lintu ex post
**Zamítnuté návrhy ze stejného důvodu:** timeout na každý krok, "aspoň jeden edit po každém čtení" (tlačí na editaci), agentní wiki fix subcommand (opět smyčka). Viz todo.md sekce "llm-wiki skill — další zlepšení".
---
## Optimalizovat skilly kvůli tokenům se nevyplatí
Celý blok skillů (~2,5k: `skills_section` 1,56k + always-skilly 0,95k) je při okně 196k jen **~1,3 % okna**. Smazat on-demand skill ušetří jen popis + framing (~4075 tok/kus) → fakticky neměřitelné. **Description neškrtat** — je to trigger pro progressive loading (model podle něj pozná, kdy skill načíst); bez něj skill přestane fungovat, ušetříš desítky tokenů a přijdeš o funkčnost. Jediná páka jsou `always: true` (jdou celým tělem), ale `my`+`memory` mají být always. Větší blok jsou tool defs (5,2k, jen vypnutím toolů v configu). **Závěr:** skilly maž podle užitečnosti, ne kvůli tokenům; reálný strop je `contextWindowTokens`, ne baseline. Začalo by to dávat smysl až u desítekstovek skillů nebo velkého těla jako `always`. Plný rozbor: [`tokens-explain.md`](tokens-explain.md).
@@ -623,6 +668,7 @@ Celý blok skillů (~2,5k: `skills_section` 1,56k + always-skilly 0,95k) je při
**`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.
@@ -742,3 +788,9 @@ Měřeno přímo proti Ollamě na `nvidia.hell` (stejný endpoint jako nanobot),
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.