runtime backup

This commit is contained in:
lachtan
2026-09-02 15:23:13 +02:00
parent 442ddc3c24
commit 812c30ed1a
17 changed files with 2238 additions and 315 deletions

View File

@@ -8,6 +8,8 @@ Ověřená fakta o vnitřním fungování nanobota. Stručně, s případným od
`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
@@ -102,14 +104,15 @@ Zdroj: `nanobot/cron/session_turns.py:is_bound_cron_job`, `nanobot/cron/bound_ru
## Workspace vzniká při prvním spuštění agenta
`~/.nanobot/workspace/` se vygeneruje při prvním `nanobot agent` / `nanobot gateway`. Obsahuje `AGENTS.md`, `USER.md`, `SOUL.md`, `HEARTBEAT.md`, `TOOLS.md`, `memory/`, git store.
`~/.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.2.0)
## 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`, `TOOLS.md`. Po editaci **není potřeba restart service** — změna platí od příští zprávy.
- Zdroj: `nanobot/agent/context.py:25` (`BOOTSTRAP_FILES`), `context.py:156` (`_load_bootstrap_files`).
- **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.
@@ -126,7 +129,7 @@ Zdroj: `nanobot/cron/session_turns.py:is_bound_cron_job`, `nanobot/cron/bound_ru
| `SOUL.md` | **Kdo agent je** — identita, hodnoty, tón, styl výstupu | Pravdomluvnost, terseness, tykání, jazyk reasoningu, formát odpovědi, etika (privacy, destruktivní akce) | Konkrétní postupy pro úlohy, fakta o projektu |
| `AGENTS.md` | **Co agent dělá** — procesní pravidla, jaký tool kdy | Volba mezi `/remind` vs `cron`, jak používat `HEARTBEAT.md`, varování typu „nepiš reminder do MEMORY.md" | Identita, hodnoty, fakta o uživateli |
| `USER.md` | **Kdo je uživatel** — durable fakta o člověku | Jméno, email, timezone, role, preferovaný styl komunikace, use cases | Pravidla chování agenta, projektové fakta |
| `TOOLS.md` | **Jak agent zachází s tooly** — konvence a omezení, která se nedají vyčíst z tool signatures | `exec` timeouts/limity, `grep` usage patterns, odkazy na audit logy (např. `log/reminder.log`) | Globální chování (to je SOUL), procesní pravidla (to je AGENTS) |
| ~~`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`) |
@@ -357,6 +360,13 @@ Dream řeší: deuplikaci, detekci stale obsahu (git blame age na řádcích MEM
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
@@ -464,7 +474,7 @@ Navazuje na sekci výše. Bound cron job **nemá vlastní session** — `payload
**Co to omezuje a co ne:**
- **`maxMessages: 120`** (server `config.json`, `agents.defaults.maxMessages` / schema default 120) — do LLM promptu se replayuje jen posledních 120 zpráv (`session/manager.py::get_history`). Token náklad per-tah tedy neroste do nekonečna, stará historie se jen vysouvá z okna.
- **`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`.
@@ -550,6 +560,30 @@ Daemon notifikuje **jen Telegram** (přes Bot API, deterministicky). Když task
---
## Prostředí `exec` toolu — PATH z procesu tam nedosáhne
`ExecTool._build_env()` (`agent/tools/shell.py`) staví prostředí subprocessu **od nuly**. Na Unixu předá jen `HOME`, `LANG`, `TERM`, `PYTHONUNBUFFERED` (+ cokoli v `tools.exec.allowedEnvKeys`). **`PATH` se z `os.environ` nekopíruje** — na Windows ano, na Unixu ne. Cokoli nastavíš v systemd unitu, `~/.profile` nebo wrapperu, `exec` neuvidí.
Do 0.2.2 to nevadilo, protože `exec` běžel jako **login shell** (`bash -lc`) a ten sourcoval `~/.profile` s `PATH="$HOME/.local/bin:$PATH"`. Upstream commit `13c951aa` (25. 6. 2026) přepnul default `login` na `False` — kvůli secrets, které se profilem vracely zpátky do prostředí. Bez login shellu se PATH dopočítá z **vestavěného defaultu bashe**:
```text
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.
```
`~/.local/bin` tam není → `uv: command not found`, exit 127.
**Jediná správná cesta je `tools.exec.pathPrepend` / `pathAppend` v `config.json`.** Hodnota musí být **adresář**, ne cesta k binárce — `/home/nanobot/.local/bin/uv` do PATH lookupu nepřispívá ničím. Implementace: `_wrap_path_export()` předřadí příkazu `export PATH="$NANOBOT_PATH_PREPEND:$PATH"; …`.
**Preferuj `pathPrepend`.** Default bashe končí `.` (aktuální adresář) a `exec` běží s CWD = workspace root, do kterého zapisuje agent i Dream — s `pathAppend` by `.` bylo v pořadí **před** našimi cestami.
**Není hot-reload.** Config watcher volá `agent.invalidate_runtime_config()` (`agent/loop.py:516`), což invaliduje jen model-runtime resolver; `ExecTool.create(ctx)` běží jednou při startu gateway. Po změně `tools.exec.*` je nutný `systemctl --user restart nanobot`.
**Protiváha — PATH procesu inertní není.** `run_cli_app` spouští CLI aplikace s `env=os.environ.copy()` (`apps/cli/service.py:1372`) a MCP stdio servery dědí taky. Proto `Environment=PATH=` v systemd unitu zůstává — jen neřeší `exec`.
Zdroj: `agent/tools/shell.py` (`_build_env`, `_wrap_path_export`, `_prepare_command`), upstream commit `13c951aa`. Diagnóza a oprava: history 2026-08-01.
---
## Skill `exec` běží z workspace rootu, ne ze skill adresáře
Když skill volá `exec` bez explicitního `working_dir`, příkaz běží s **CWD = workspace root** (`~/.nanobot/workspace`), **ne** v adresáři skillu. Cesty na skripty skillu proto musí být buď workspace-relativní (`skills/<name>/scripts/x.py`) nebo absolutní — **skill-dir-relativní `scripts/x.py` se rozbije** (resolvuje na `workspace/scripts/x.py`).
@@ -655,7 +689,7 @@ On-demand skill pro okamžitou explicitní paměť. Uživatel řekne „keep X"
**Ukládá i *why*, ne jen *what* (od 2026-06-06):** Krok 2 Write protokolu rozlišuje typ záznamu — plain fakt (alergie, deploy window, jméno) jde bez důvodu; **rozhodnutí / preference / dead-end** dostane důvod inline na stejném řádku (`<fakt> — because <terse why>`). Pokud je vstup rozhodnutí/dead-end *bez* uvedeného důvodu, model se **jednou doptá** na why (decline/self-evident → uloží bez něj). Záměrně úzká varianta Claude memory.md vzoru, který why přidává jen u feedback/project, ne u reference/faktu. Žádné `Why:` bloky ani few-shot příklady — silné Ollama Cloud / OpenRouter modely zvládnou hranici fakt-vs-rozhodnutí zero-shot. Plný kontext: history.md 2026-06-06.
**Gotcha — BOOTSTRAP_FILES jsou hardcoded:** `nanobot/agent/context.py:25` má `BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"]` — nelze přidat vlastní soubor bez patche. Vše, co má být vidět každý tah bez on-demand loadingu, musí být reference v existujícím bootstrap souboru (USER.md, SOUL.md, …).
**Gotcha — BOOTSTRAP_FILES jsou hardcoded:** `nanobot/agent/context.py:57` má `BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md"]` (do 0.2.x i `TOOLS.md`) — nelze přidat vlastní soubor bez patche. Vše, co má být vidět každý tah bez on-demand loadingu, musí být reference v existujícím bootstrap souboru (USER.md, SOUL.md, …).
---
@@ -671,7 +705,7 @@ Přepsáno z SQLite row-store na **capture → compile pipeline** (vzor llm-wiki
---
## Skill `/project` — pojmenované dlouhodobé pracovní kontexty (přepsáno 2026-07-22)
## 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.
@@ -679,6 +713,10 @@ Substituce Claude.ai "Projects". Adresář na projekt, ne jeden soubor: `workspa
**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á **stdin quoted heredocem** (`<<'NOTE'`), ne `--text` — shell obsah neinterpretuje, takže `„"`, `'` i `"` projdou doslova (v `log/note.log` je doložený případ, kdy model `--text` argument zmršil na `...`). Stav skriptu přepisuje env `PROJECTS_DIR` (testy). Plný kontext: history.md 2026-09-02.
**Projektová data nemají strop ani konsolidaci** — `memory.md` roste neomezeně a nikdy se nekomprimuje ani nearchivuje. Velikost se řeší **výhradně na straně čtení**: `activate` nad limitem tool výsledku vypustí z výstupu nejstarší záznamy a ukáže cestu k plnému logu, soubor na disku nechá beze změny. Zamítnutá varianta: prahy 8 000 / 12 000 znaků s nabídkou konsolidace — ztráta zadaného obsahu je horší failure mode než jakákoli úspora kontextu (a čísla stála na špatném okně, viz níže).
## `my` nástroj — přece jen nějaký perzistentní scratchpad existuje
Zjištěno 2026-07-22 při objevu starého `/project` skillu výše: ten používal `my(action="set", key="project_context", value="<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.
@@ -722,6 +760,8 @@ 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.
**Gotcha — `agents.defaults.contextWindowTokens` je zavádějící číslo.** V serverovém `config.json` je `65536`, ale to platí jen když preset vlastní hodnotu nemá. Reálné okno aktuálního defaultu (**`glm53`: 976 000**, `glm52`/`glm-flash` 976 000, `kimi3` 1 020 000, `sonnet`/`gemini-flash` 256 000) je ~15× větší, než `defaults` napovídá. **Než z 65k něco odvodíš, přečti `model_presets`, ne `agents.defaults`** — na tomhle jsem 2026-09-02 postavil celý (zamítnutý) rozpočet velikosti pro `/project`. Limit, který v praxi kouše, je `maxToolResultChars: 16000`, ne okno.
**Důsledky (trade-off, ne čistá výhra):**
- **+** Méně ořezávání/komprese historie → lepší návaznost v dlouhých sezeních. Delší souvislé odpovědi (16k vs 8k output).
@@ -912,3 +952,196 @@ K tomu už dřív známé: tool-result bug + výrazná pomalost. **Zkouší se n
**Problém → příčina → fix:** Modely (kimi27 i default) po `/remind list` dostanou kompletní tabulku (CLI exit 0), ale jeden ji scvrkne na počet, druhý si ji přerenderuje po svém. → Příčina: `SKILL.md` sekce `## Behavioral contract` *popisovala formát* výstupu, ale neříkala „předej kompletní" — modely to čtou jako surová data k vlastnímu formátování. → Fix: odstavec **Showing read results** (`list`/`upcoming`/`delivered` = user-ready text, vypsat každou položku s `#display-id`, nesumarizovat). Plný záznam: history 2026-06-15 13:21.
**Diagnostika chování nanobot agenta = číst webui session logy.** Konverzace (user/assistant/tool turny, včetně `reasoning_content` a `exec` výstupů) žijí na serveru ve `workspace/sessions/websocket_<id>.jsonl` (kopie i v `~/.nanobot/webui/`). Pro „proč model udělal X" stáhnout příslušnou session a číst turny — odhalí, že příkaz uspěl a chyba je až v prezentaci.
## Session logy: 89 % objemu jsou tool výsledky
Z 534 souborů v `~/.nanobot/workspace/sessions/` je 332 reálných konverzací (12 311 zpráv, 14,1 MB obsahu). **12,5 MB (89 %) tvoří návratové hodnoty toolů** — už oříznuté na `maxToolResultChars: 16000`. Pro analýzu chování stačí nahradit je metadaty (`name(args) → ok|ERROR, velikost`), čímž korpus spadne na ~3 MB. Přírůstek je 4,4 session/den, aktivita jen 70 % dní. Zdroj: měření 2026-09-01, `skills/reflect/scripts/reflect_distill.py`.
## `reflect`: nálezy platí pro okno, ne pro celou historii (2026-09-02)
Dokud běh dohání backlog chronologicky, nálezy popisují **nejstarší** nezpracované session,
i když je předkládá jako aktuální. Reálně: 3 běhy zpracovaly 56 z 356 session, všechny
z 26.29. 5., a všech 8 rozhodnutých nálezů (4 aplikované do `SOUL.md`) tak opravovalo
chování z konce května. Fix: `since = max(cursor, now - window)` — cursor je **podlaha**
(nic dvakrát, jinak `merge_findings` sečte počty znovu), okno **strop** (starší session se
přeskočí natrvalo). Default `--window-days 21`. Plný záznam: [history.md](history.md) 2026-09-02.
Dávkování samo nálezy nezkreslovalo — každý z těch tří běhů byl jedna dávka.
## `reflect`: destilát session vyrostl z ~5 kB na ~14 kB (měřeno 2026-09-02)
21denní okno = 74 session = **6 dávek** po 200 kB (~1 MB destilátu). Původní plán počítal
s ~4,4 session/den po ~5 kB, tedy jednou dávkou. Důsledek: `--deadline-minutes 20` +
`TIMEOUT_SECONDS 45 min` zvládnou 12 dávky za noc, takže okno se dohání ~2,4 dne za noc.
Při dimenzování dávkovaného běhu nad session logy je tedy nutné velikost **měřit**
(`reflect_distill.py --stats`), ne odvozovat z počtu session.
## `reflect`: počty v nálezu jsou self-report modelu, ne měření
`occurrences` / `sessions_affected` si vymýšlí analyzující LLM a `merge_findings` je jen
sčítá napříč běhy. Nekoherenci to propustilo do store: nález `f09a7` měl `occurrences: 4`
a `sessions_affected: 5`. Skript umí zkontrolovat jen invarianty
(`sessions_affected ≤ min(occurrences, počet session v dávce)`, clamp ve validátoru) —
absolutní hodnota zůstává nedokázaná a v review se nesmí prezentovat jako měření.
## `reflect`: dávkovaná analýza nevidí rozptýlené vzory
Prompt zakazoval hlásit jednorázový slip, takže vzor s frekvencí ~1× na dávku se nikdy
nepojmenoval → nikdy nespočítal → práh `≥2 výskyty a ≥2 session` nepřelezl. Práh sám
skládání napříč dávkami zvládá; chyběl mu vstup. Fix: prompt smí hlásit jediný výskyt
vzoru, který **už je v „Known patterns"**, a dostal informaci, že vidí jen výsek historie.
Novost vzoru se tím nemění — genuinely nový singleton se pořád nehlásí.
## `reflect`: zápis do auditního store patří výhradně skriptu
`SKILL.md` říkal agentovi, aby u nálezu bez patche „propose the exact old_text/new_text
yourself", ale `reflect_apply.py` patch přijmout neuměl (`--new-text-file` jen přepíše
`new_text` **už existujícího** patche). Agentovi nezbylo než editovat `findings.jsonl`
ad-hoc skriptem — session `7a988478` na to spotřebovala 3 jednorázové skripty. Fix
(2026-09-02): akce `--set-patch <json>`, která kandidáta nejdřív prožene `check_patch()`
a **teprve pak** zapíše, plus věta ve STOP gate 4, že se store needituje ručně nikdy.
Poučení obecně: dokud pro nějaký legitimní krok neexistuje volání skriptu, prompt ho
nezakáže — agent si cestu najde a bude mít pravdu.
## `reflect`: „naposledy" u nálezu byl dřív datum přepsání záznamu
`_seen_line()` vydávalo `created` (kdy se záznam naposledy složil) za „last seen", takže
nález refilovaný každou noc hlásil dnešek bez ohledu na stáří důkazů. Ze stejného zdroje
plynuly falešné regrese: `regression_of` se nastavilo, kdykoli se vzor po `applied` objevil,
i když analyzovaná session byla z doby **před** opravou (`retry-without-diagnosis` 67×
označen REGRESE, nejnovější důkaz 31. 8., patch 1. 9.). Fix (2026-09-02): derivované pole
`last_seen` = `max(evidence[].when)` (jen tvary `^\d{4}-\d{2}-\d{2}`, prvních 10 znaků,
fallback `created`) — a `regression_of` jen když `last_seen > applied.at[:10]`, jinak `watch`.
`when` je volný string od modelu a formáty se míchají (`2026-08-31` i `2026-08-31 13:53`).
## `reflect`: report počítal nálezy po dávkách, ne po vzorech
`_run()` dělalo `merged += batch_findings`, takže vzor nalezený v 5 dávkách byl v `merged`
5× — včetně mezistavů, které fold zahodil. Z toho žil report, `stats` i Telegram: report
z 2. 9. měl **29 nadpisů proti 13 vzorům ve store**. Fix (2026-09-02): `merged` je
`dict[str, Finding]` klíčovaný `pattern`, poslední zápis (nejvíc složený) vyhrává.
Store byl správně po celou dobu — nafouknutá byla jen prezentace.
## Vyřešené chyby ve skillu `reflect` (2026-09-02) — noční běh nedobíhal
Noční cron padal na 30min timeout a zahazoval i to, co už měl hotové:
- **All-or-nothing zápis** → `_write_findings`, kurzor i report se dělaly po poslední dávce,
takže timeout zahodil dvě dávky s platnými nálezy a kurzor nechal na místě → další noc totéž
plus nové session → fix: `commit()` po **každé** dávce, kurzor se nikdy nepohne dozadu.
- **Žádný strop na délku běhu** → 412 session za kurzorem = 7 dávek ≈ 90 min proti 30 min
→ fix: soft deadline `--deadline-minutes` (default 20), `TIMEOUT_SECONDS` 45 min už jen jako
brzda na zaseknutou dávku.
- **185k-tokenový prompt přetékal provider timeout** (`_OPENAI_COMPAT_REQUEST_TIMEOUT_S` = 120 s,
`NANOBOT_LLM_TIMEOUT_S` = 300 s), retry zahodí hotový prefill a začne znovu → 9 timeoutů za běh
→ fix: `os.environ.setdefault` na 600/900 s + dávka 500 kB → 200 kB (~70 k tokenů).
- **Infra chyba šla do JSON validátoru** → nanobot vrací `Error calling LLM: …` jako odpověď
agenta, validátor to hlásil jako „invalid JSON" a spotřeboval 1 ze 3 pokusů, model dostal
vytýkáno něco, co nenapsal → fix: `RunResult.stop_reason == "error"` se pozná dřív, přepošle
se **původní** prompt v čerstvé session, vlastní strop 2 pokusy.
Efekt: dávka z ~13 min na **147 s**, jedna iterace, žádný retry. Plný záznam:
[history.md](history.md) 2026-09-02.
## `RunResult` z `Nanobot.run()` rozlišuje selhání providera
`nanobot/sdk/types.py:50``RunResult``stop_reason` a `error`. Když LLM call selže, nanobot
vrátí text chyby jako **obsah odpovědi** (`Error calling LLM: timed out after 300s`) a zároveň
nastaví `stop_reason="error"` + `error` (`agent/runner.py:289-305`, `agent/loop.py:1019`).
Skript, který parsuje odpověď agenta, musí tenhle stav testovat **před** validací — jinak
diagnostikuje infra výpadek jako vadný výstup modelu.
## Timeouty LLM callu v nanobotu: 120 s na request, 300 s na celý call
Dvě vrstvy, obě přebitelné env proměnnou:
| Vrstva | Default | Env | Zdroj |
|---|---|---|---|
| jeden HTTP request na OpenAI-kompatibilní endpoint | **120 s** | `NANOBOT_OPENAI_COMPAT_TIMEOUT_S` | `providers/openai_compat_provider.py:89,163` |
| celý call včetně retry (3 pokusy) | **300 s** (0 = vypnuto) | `NANOBOT_LLM_TIMEOUT_S` | `agent/runner.py:702` |
| idle mezi chunky ve streamu | 90 s | `NANOBOT_STREAM_IDLE_TIMEOUT_S` | `providers/base.py:20` |
SDK cesta (`Nanobot.run()`) **nestreamuje** (`stream: False`), takže idle timeout se jí netýká.
Retry počítá prefill znovu od nuly — u desítek tisíc tokenů promptu je tedy dražší než čekání,
proto se u dávkových skriptů vyplatí request timeout zvednout, ne zkracovat.
## `reflect`: `--new-text-file` bez `--check` aplikuje okamžitě (2026-09-02)
`reflect_apply.py --new-text-file <path>` **není náhled** — bez `--check` jde přímo do
`apply_finding()`, tedy zápis souboru + commit. Náhled uživatelovy editace je až
`--check --new-text-file`, a protože se jeho text do `patch` neukládá, musí temp soubor
přežít do aplikace a předat se znovu. `SKILL.md` to mělo v tabulce rozhodnutí zaměněné
(řádek `edit:` sliboval diff, předepisoval aplikaci) — opraveno, viz history 2026-09-02 14:55.
## Vyřešené chyby ve skillu `reflect` (2026-09-01)
Čtyři tiché chyby — nic nespadlo, jen se dělo něco jiného, než co slibovala dokumentace:
- **Kurzor nefiltroval** → `_run` ukládal zformátovaný stamp (`2026-07-11 14:02`), `collect_sessions`
ho porovnávala proti syrovému ISO (`…T09:00:…`); `'T'` > `' '`, takže 9 session z dne kurzoru se
analyzovalo znovu každý běh a výskyty se počítaly dvakrát → fix: `SessionDigest.started` drží syrové
ISO, formátuje se až v hlavičce.
- **Zamítnutí vydrželo jen jeden běh** → `merge_findings` brala `previous` jako záznam s nejnovějším
`created`, takže `watch` záznam založený po zamítnutí přebil `rejected` → fix: zamítnutí je vlastnost
vzoru (set přes všechny záznamy), ne posledního záznamu. Stejná příčina ztrácela `regression_of`.
- **`reflect_apply.py` hlásil „refused" po zápisu** → soubor se přepisoval před commitem, selhání
`git commit` vrátilo exit 2 se změněným souborem → fix: rollback na původní obsah, který
`check_patch` už vracel.
- **Validátor zahazoval celou dávku** kvůli neznámému klíči nebo o 10 znaků delší diagnóze → retry
přeposílal ~420k tokenů → fix: retry jen při neparsovatelném JSON nebo když nepřežil ani jeden nález.
Plný záznam: [history.md](history.md) 2026-09-01.
## Test může projít kolem chyby, když si vstup vyrobí v jiném formátu než volající
`test_since_excludes_already_processed_sessions` podával `since` jako `"2026-08-01T00:00:00"` — formát,
který produkční kód nikdy nevyrobí (ukládal `"2026-08-01 00:00"`). Test byl zelený a chyba běžela
v produkci. **U hodnot, které jedna část kódu zapisuje a druhá čte, testuj round-trip, ne literál.**
Zdroj: `skills/reflect/tests/test_reflect_distill.py`, history 2026-09-01.
## Tokenizace nanobot korpusu: ~1,2 znaku na token
Naměřeno na reálném destilátu (90 kB promptu → 75 135 tokenů podle tiktoken v `nanobot.agent.memory`). Čeština, názvy toolů a UUID se tokenizují špatně. **Odhad „34 znaky na token" je pro tenhle korpus 3× mimo** — kdo počítá velikost dávky, musí použít 1,2.
## `contextWindowTokens` presetu přebíjí `agents.defaults`
`agents.defaults.contextWindowTokens` je 65536, ale preset `glm53` má 976000 a **vyhrává**`agent/loop.py:476` (`context_window_tokens = extra.pop(...) or resolved.context_window_tokens`). Efektivní input budget = `contextWindowTokens - maxTokens - 1024` (`SNIP_SAFETY_BUFFER`, `agent/context_governance.py:105`), tedy ~958k tokenů pro glm53.
## Velká zpráva projde, `snip_history` krátí jen historii
`ContextGovernor.snip_history` (`agent/context_governance.py:383`) zahazuje **celé starší zprávy**, nikdy nekrátí jednu zprávu — a nejnovější zprávu přidá vždy, i když sama překročí budget (`if kept and kept_tokens + msg_tokens > remaining_budget: break`, řádek 422). Velký vstup je proto lepší poslat **přímo ve zprávě** než souborem přes `read_file`, který by ho uřízl na `maxToolResultChars` (16 000 znaků).
## `uv` není v PATH neinteraktivního SSH
`ssh nanobot@nanobot.hell 'uv run …'` skončí `failed to run command 'uv': No such file or directory`. Je v `/home/nanobot/.local/bin/uv` — přes SSH je nutná plná cesta. **Crontab si `PATH` nastavuje sám** (`PATH=/home/nanobot/.local/bin:/usr/bin:/bin`), tam bare `uv run` funguje. Souvisí s `tools.exec.pathPrepend` (history 2026-08-01).
## LLM rozbíjí JSON českou uvozovkou — a hláška o tom musí být konkrétní
GLM-5.3 při psaní české diagnózy do JSON stringu napsal `(„repeated external lookup blocked")`: otevírací uvozovka je `„` (U+201E), ale **zavírací je ASCII `"`**, která neescapovaná ukončí string. Generická hláška „no parseable json block" nedá retry nic použitelného a všechny 3 pokusy selžou stejně. Fix: validátor hlásí `msg`, řádek, sloupec a výřez textu okolo `error.pos`, plus prompt zakazuje uvozovky uvnitř string hodnot. Po opravě prošel první pokus. Zdroj: `skills/reflect/scripts/reflect_auto.py:_decode_payload`, history 2026-09-01.
## `~/.nanobot/workspace` je git repo a nanobot do něj commituje sám
Lokální repo **bez remote**; 15 z posledních 20 commitů je `nanobot <nanobot@dream>` (Dream dělá `dream: periodic memory consolidation`). `.gitignore` vynechává `db/ sessions/ log/ tmp/ backup/ tasks/ cron/runs/`. Důsledek: skill, který mění soubory na serveru, nepotřebuje vlastní zálohy — commit před změnou a `git revert` stačí. Pozor: **není to záloha mimo stroj** a je to repo nespojené s `src/nanobot`.
## Stav skillu nesmí bydlet v adresáři skillu
`rsync -av skills/<name>/ …` přepisuje celý adresář, takže `state.json` nebo databáze uvnitř `skills/<name>/` se při nasazení ztratí. Data patří do vlastního adresáře v rootu workspace — vzor `skills/note/` + `notes/`, nově `skills/reflect/` + `reflect/`.
## `reflect`: odhad šance opravy stojí na tom, jestli je text v kontextu (2026-09-02)
`/reflect` ukazuje u nálezu odhad `~80 / ~60 / ~40 / ~20 %`, že oprava vzor skutečně
zastaví. Hlavní osa rubriky není kvalita formulace, ale **jestli je opravovaný text
v kontextu ve chvíli, kdy chyba vzniká**: gate ve skriptu drží vždy (~80 %), tvrdý zákaz
v `SOUL.md`/`AGENTS.md`/`SKILL.md` dotčeného skillu ~60 %, přeformulování tamtéž ~40 %,
soubor mimo kontext nebo ponechání na úvaze agenta ~20 %. `regression_of` sráží o pásmo —
instrukce toho druhu už na tom vzoru jednou selhala. Pásma, ne přesná čísla: je to odhad
ze záznamu, ne měření. Zdroj: `skills/reflect/SKILL.md` sekce „Estimating the odds",
history 2026-09-02.
## `reflect`: `--workspace` umožní zkoušet zásahy mimo ostrý store (2026-09-02)
`reflect_apply.py --workspace <cesta>` bere kompletní workspace odjinud, takže se dá
`--set-patch`/`--check`/`--apply` vyzkoušet na kopii (`findings.jsonl` + cílový soubor
v `tmp/`) a ostrý store i cílový soubor zůstanou nedotčené. Použito při ověření draftování
patche na reálném nálezu `f5c34`. Zdroj: `skills/reflect/scripts/reflect_apply.py:250`,
history 2026-09-02.