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

@@ -1,5 +1,134 @@
# History
## 2026-09-02 — reflect: noční běh padal na timeout a zahazoval hotové dávky
**Cíl:** Noční cron (`30 3 * * *`) neodběhl — ve 4:00 přišlo na Telegram
`reflect: ERROR — timeout po 30 min`. Zjistit proč a opravit.
**Co jsem zkusil / co jsem našel:**
Rozbor `log/reflect_cron.log` a `reflect/state.json` dal pět příčin, které se násobily:
1. **Cron žral celý backlog, ne denní přírůstek.** Za kurzorem (`2026-05-27T14:39`) leželo
412 session = 7 dávek po ~500 kB ≈ 90 min práce proti 30min stropu. Ostrý běh 1. 9. byl
omezený `--max-batches 1`, cron žádný strop neměl → deterministické selhání každou noc.
2. **All-or-nothing zápis.** `_write_findings`, kurzor i report se dělaly teprve po poslední
dávce. Dávky 0 a 1 přitom **vrátily platné nálezy** (03:38 a 03:55) — timeout je zahodil
a kurzor nechal na místě, takže druhý den totéž plus nové session. Ráčna, která se sama
nerozjede.
3. **Requesty na hraně timeoutu.** Prompt 185 k tokenů, `_OPENAI_COMPAT_REQUEST_TIMEOUT_S`
je 120 s a `NANOBOT_LLM_TIMEOUT_S` 300 s. 9× timeout; retry zahodí hotový prefill
a začne od nuly, dva tahy (600 s) skončily `Error calling LLM: timed out after 300s`.
4. **Infra chyba se tvářila jako vadná odpověď.** Ten error text šel do `parse_findings`
→ „the JSON is invalid" + zavádějící hint o uvozovkách → spotřeboval 1 ze 3 JSON pokusů
za dávku, a model dostal vytýkáno něco, co nenapsal.
5. **Tool cally v analytickém tahu** (`read_file SOUL.md`, `skills/*/SKILL.md`, `grep`) —
každá iterace je další plný prefill, 24 min na dávku.
**Co fungovalo a proč:**
- **Zápis po každé dávce** (`commit()` uvnitř smyčky v `_run`): nálezy, kurzor, report
i záznam běhu jdou na disk hned. Kurzor se navíc nikdy nepohne dozadu (`max` proti
stávající hodnotě), aby spadlý `--all` neshodil noční postup.
- **Soft deadline** `--deadline-minutes` (default 20) — nová dávka nezačne po limitu,
`TIMEOUT_SECONDS` zvednut na 45 min a je teď jen brzda na zaseknutou jednu dávku.
Když běh nedojde na konec, do Telegramu se přidá `Zpracováno N/M dávek.`
- **Infra chyba ≠ vadná odpověď:** `RunResult.stop_reason == "error"` (resp. `.error`) se
pozná dřív než validátor, nespotřebuje JSON pokus a přepošle **původní** prompt v čerstvé
session (selhaný tah nechal v té staré error zprávu i celý destilát). Strop 2 pokusy, pak
`ReflectError("model nedostupný: …")`.
- **Timeouty přes env** v hlavičce skriptu (`os.environ.setdefault`):
`NANOBOT_OPENAI_COMPAT_TIMEOUT_S=600`, `NANOBOT_LLM_TIMEOUT_S=900`.
- **Menší dávky:** `DEFAULT_BUDGET_CHARS` 500 000 → 200 000 (~70 k tokenů).
- **Strop na čtení souborů** v promptu („Read at most 2 files").
**Ověření (ostrý běh 06:51):** 26 session, jedna dávka, **147 s**, 83 k prompt tokenů,
**jedna iterace, žádný tool call, žádný provider timeout, JSON dobrý na první pokus**
(dřív ~13 min na dávku). Kurzor se posunul `2026-05-27T14:39``2026-05-29T09:21`,
`findings.jsonl` má 3 `open` + 4 `watch`, report `results/2026-09-02_reflect.md` sedí,
`state.json` má záznam běhu s `batches: 1`. Lokálně 115 testů zelených (11 nových), ruff čistý.
**Co zbývá:**
- Nechat proběhnout noční cron a ráno ověřit, že kurzor pokročil a nepřišel ERROR.
- Backlog (~386 session) se teď vysype po dávkách sám; kvalitu nálezů projít přes `/reflect`.
- **Pozor na falešné regrese:** analýza starých session hlásí `regression_of` u vzorů
opravených 1. 9. (`answer-self-config-from-guesswork`), protože ty session jsou z května,
tedy z doby před opravou. Regresní příznak má smysl teprve u session novějších než `applied`.
- Patche: tenhle běh nevrátil ani jeden (dřív 1 z 8). Sledovat, jestli to je náhoda, nebo
cena za strop na čtení souborů.
**Jak to vrátit zpět:** `git revert` commitu se změnou skillu v tomto repu + rsync na server
(`rsync -av --exclude '__pycache__' --exclude '.pytest_cache' skills/reflect/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/reflect/`).
Kurzor v `reflect/state.json` na serveru se dá vrátit ručně na `2026-05-27T14:39:22.120065`,
nálezy z běhu 06:51 mají `created: 2026-09-02` (7 řádků v `findings.jsonl`).
## 2026-09-01 — reflect: review skillu a oprava tří tichých chyb + zjednodušení validátoru
**Cíl:** Uživatel chtěl důkladné review skillu `reflect` se zaměřením na to, jestli jsou skripty
zbytečně dlouhé a složité. Review našlo tři tiché chyby (nic nespadne, jen se děje něco jiného, než
co slibuje dokumentace) a čtyři místa zbytečné složitosti. Plán schválen, pak vykonán.
**Co jsem zkusil / co fungovalo a proč:**
1. **Kurzor nefiltroval.** `_run` ukládal `digest.started` zformátovaný (`2026-07-11 14:02`), ale
`collect_sessions` ho porovnávala proti syrovému `created_at` (`2026-07-11T09:00:12.345678`).
`'T'` (0x54) > `' '` (0x20) → porovnání vždycky vyšlo „novější". Ověřeno na 332 reálných session:
se starým formátem se **9 session z dne kurzoru analyzovalo znovu každý běh**, výskyty se počítaly
dvakrát a vzor se povyšoval na `open` dřív, než skutečně nastal podruhé — přesně to, čemu má práh
bránit. Fix: `SessionDigest.started` drží syrové ISO, formátuje se až v hlavičce destilátu.
Unit test to nechytal, protože si `since` podával jako literál v ISO — ve formátu, který volající
nikdy nevyrobí. Nový test dělá round-trip, který dělá `_run`.
2. **Zamítnutí vydrželo jen jeden běh.** `merge_findings` hledala `previous` jako záznam s nejnovějším
`created`. Po zamítnutí vznikl nový `watch` záznam s dnešním datem, ten příště přebil `rejected`
a vzor se otevřel. Reprodukováno: běh po zamítnutí → `watch`, o běh dál → `open`, přestože README
slibuje „zamítnutý vzor se znovu neotevře". Na serveru leží 4 zamítnuté vzory, takže by to bylo
vidět hned při backlog běhu. Fix: zamítnutí je vlastnost *vzoru* (`rejected_patterns` set přes
všechny záznamy), ne posledního záznamu. Stejná příčina brala i `regression_of` — přenáší se dál.
3. **`reflect_apply.py` hlásil „refused" po tom, co už soubor změnil.** Zápis byl před commitem, takže
selhání `git commit` vrátilo exit 2 se změněným necommitnutým souborem, bez audit řádku a s nálezem
pořád `open`. SKILL.md přitom agentovi říká „exit 2 = odmítnuto, jdi dál". Fix: rollback na původní
obsah (`check_patch` ho vrací, jen se zahazoval) + best-effort odstagování, původní chyba se
propaguje beze změny.
4. **Validátor zahazoval celou dávku.** Neznámý klíč, o 10 znaků delší diagnóza nebo 13 nálezů místo 12
vyhodily `FindingsError` a přeposlaly celý prompt — **~500 kB ≈ 420k tokenů**. Plán přitom říká
„zahodí vadné". Nově: retry jen když se nedá naparsovat JSON nebo nepřežil ani jeden nález; vadný
nález se zahodí, vadný patch se zahodí jen patch (diagnóza a návrh mají cenu i bez něj), dlouhý text
se ořízne, přebytečné nálezy se useknou. Důvody jdou na stderr, ať zahazování není neviditelné.
5. **Mrtvé a nekonzistentní věci:** `known-patterns.md` se generoval, ale nikdo ho nečetl (slovník pro
prompt se počítá přímo z `findings.jsonl`) — zrušen včetně řádků v obou dokumentacích.
`--check` existoval, ale SKILL.md místo něj instruoval agenta ověřit patch ručně — teď tiskne diff
a skill ho jen ukazuje. Dva různé výchozí budgety (1,5 M vs 500 k) sjednoceny na jednu konstantu.
`_session_start` četl celý JSONL kvůli pěti záznamům. Report dostal metriku „výskyt známých vzorů
na 100 session", kterou plán chtěl a nikdy nedostal.
**Co jsem záměrně neudělal:** přepis `Finding` dataclass na dicty (ušetřilo by ~45 řádků, ale je to
čistá kosmetika sahající na většinu prahových testů) a změnu struktury `_run` kvůli 30min timeoutu
(backlog jde bezpečně po dávkách přes `--all --max-batches 2`, jen to nebylo nikde napsané — doplněno
do README).
**Verifikace:** 105 testů zelených lokálně i na serveru (bylo 96; 9 nových pokrývá přesně ty tři chyby
a nové chování validátoru). End-to-end na 332 reálných session: 1. běh 332 session → kurzor, 2. běh
0 session; částečný běh po 2 dávkách pokračuje s nulovým překryvem. `ruff check` + `ruff format` čisté.
Dry-run na serveru: 337 session v 7 dávkách po ≤499 kB, `git status` workspace 18 souborů před i po
(nic se nezměnilo).
**Nasazení:** `rsync` na server (drift proti repu žádný — ověřeno před přepisem). Smazán mrtvý
`reflect/known-patterns.md`. **Kurzor v `reflect/state.json` přepsán** z `2026-05-27 14:39` na
`2026-05-27T14:39:22.120065`, jinak by se ten den zpracoval ještě jednou.
**Co zbývá:** `reflect/` na serveru **není v gitu** — ani trackované, ani ignorované, přestože plán
počítal s tím, že bude verzované jako auditní stopa nálezů a rozhodnutí. Rozhodnutí na uživateli.
**Jak vrátit zpět:** `git revert 46fe20f` (skill) a `git revert 69fe364` (rsync exclude v CLAUDE.md),
pak `rsync -av --exclude '__pycache__' --exclude '.pytest_cache' skills/reflect/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/reflect/`.
Kurzor zpět: přepsat `reflect/state.json` na `2026-05-27 14:39`. `known-patterns.md` se obnoví sám
jen s revertovaným kódem.
## 2026-07-02 — bookmark: zestručnění frontmatter description (routing-focused), nasazeno
**Cíl:** Popis skillu byl moc dlouhý, prozaický a obecný („Manage a personal reading list…"). Přepsat na krátký, foldovaný (`>`) a zaměřený na *kdy aktivovat*, ne na výčet funkcí.
@@ -2327,3 +2456,601 @@ Běh 5 modelů s gate=3:
**Co zbývá:** Model je do dobití extra usage na ollama.com nepoužitelný. Alternativa bez dobíjení, pokud ji uživatel bude chtít: preset přes OpenRouter (`moonshotai/kimi-k3`, ctx 1 048 576, $3/M in + $15/M out) — provider `openrouter` je v configu už nakonfigurovaný. Nezakládal jsem ho, nebylo zadáno.
**Jak vrátit zpět:** `ssh nanobot@nanobot.hell 'cp ~/.nanobot/backup/config.json.bak-20260727-202245 ~/.nanobot/config.json'` — mimo tuhle jednu hodnotu jsem na serveru nic neměnil.
## 2026-08-01 — Upgrade nanobot 0.2.2 → 0.3.0: rozbité `uv` v `exec`, osiřelý `TOOLS.md`, mrtvý `maxMessages`
**Cíl:** Po upgradu na 0.3.0 (31. 7. 15:42, restart 16:58) přestalo v chatu fungovat `uv run` — `/remind list` padal na `uv: command not found`, exit 127. Zadání: zjistit příčinu, pak opravit všechny dopady upgradu včetně `TOOLS.md` a `agents.defaults.maxMessages`.
**Co jsem zkusil (diagnóza):**
- `ssh` průzkum: `uv` je v `/home/nanobot/.local/bin/uv`, systemd unit má vlastní `Environment=PATH` bez `~/.local/bin`.
- Stažení nainstalovaného balíčku 0.3.0 (`rsync` site-packages do `tmp/srv-0.3.0/`) a čtení `agent/tools/shell.py`.
- Klon upstreamu do `tmp/nanobot-upstream/`, `git log -S` nad `shell.py`, `context.py`, `config/schema.py` mezi `v0.2.2..v0.3.0`.
- Reprodukce exec prostředí přes `env -i HOME=… /bin/bash -c` vs `-lc` (s a bez `NANOBOT_PATH_*`).
- Analýza `sessions/*.jsonl` — kdy a jak selhání vypadala.
**Co fungovalo a proč — tři nezávislé regrese:**
1. **`uv` v `exec`.** Upstream `13c951aa` (25. 6., *„change exec login-shell default from true to false"*) přepnul default `login` z `True` na `False` — profil sourcovaný login shellem vracel do exec prostředí secrets, které `_build_env()` schválně vyhazuje. Jenže `_build_env()` na Unixu **PATH vůbec nepředává** (jen `HOME`/`LANG`/`TERM`/`PYTHONUNBUFFERED`), takže bez `bash -l` se PATH dopočítá z vestavěného defaultu bashe `/usr/local/bin:…:/bin:/sbin:.`, kde `~/.local/bin` není. Odhalilo to **existující chybu v našem configu**: `tools.exec.pathAppend` obsahoval `/home/nanobot/.local/bin/uv` — cestu k **binárce**, ne k adresáři, takže do PATH lookupu nepřispíval ničím. Do 0.2.2 to maskoval login shell.
- Naměřeno: `bash -c` + starý `pathAppend` → `uv` NENALEZEN; `bash -lc` + tentýž `pathAppend` → nalezen; `bash -c` + adresář → nalezen.
- Rozsah: `remind`, `note`, `llm-wiki`, `python`. Nezasažené: crontab (vlastní `PATH=`), `bookmark` (absolutní cesta v SKILL.md), `detach` (systemd unit s PATH).
- **„Zlobí jen u některých modelů" byl klam.** Selhává každé bare `uv run`; liší se jen schopnost modelu se vzpamatovat. Model, který zkusil `PATH="$HOME/.local/bin:$PATH" uv run …`, uspěl; modely, které zkusily `which uv || find /home -name uv`, narazily na `restrictToWorkspace: true` (`Command blocked by safety guard`) a vzdaly to.
2. **`TOOLS.md` osiřel.** Upstream `d29fcaf5` (21. 5., *„internalize tool contract prompt"*) přesunul `templates/TOOLS.md` → `templates/agent/tool_contract.md` a rendruje ho přímo do promptu; `BOOTSTRAP_FILES` je nově `["AGENTS.md", "SOUL.md", "USER.md"]`. Obsah tedy nezmizel — naopak je bohatší — ale přestal být uživatelsky editovatelný. Náš workspace soubor s vlastními dopisky zůstal ležet a nikdo ho nečetl.
3. **`maxMessages` mrtvý.** Upstream `dacc6992` (29. 6.) klíč vyřadil ze schématu a přidal do `_migrate_config()` shim, který ho zahodí a zaloguje warning. Spamoval journal — **145 výskytů za hodinu**.
**Proč u `TOOLS.md` neproběhla migrace, a u `maxMessages` ano:** nanobot migruje **config**, ne **workspace**. `sync_workspace_templates()` má v docstringu doslova *„Creates missing files without overwriting user files"* — jen dotváří chybějící, nikdy nepřepisuje ani nemaže, protože workspace `*.md` jsou user data. Když šablona zmizí z balíčku, kopie ve workspace prostě zůstane, tiše. Config naopak shim + warning dostane (a `281b4b7f` ukazuje, že se ty shimy po verzi systematicky mažou).
**Co jsem změnil:**
- `~/.nanobot/config.json`: `tools.exec.pathAppend` → **`pathPrepend`** = `/home/nanobot/.local/bin:/home/nanobot/.nvm/versions/node/v24.16.0/bin` (adresář místo binárky); odstraněn `agents.defaults.maxMessages`. Zálohy `backup/config.json.bak-20260801-0703{54,}` a `-070420`. *(Obě změny nakonec zapsal uživatel sám v 07:03:54, můj skript je zastihl už hotové — ověřil jsem výsledek.)*
- `~/.config/systemd/user/nanobot.service`: `ExecStart` zpět na `/home/nanobot/.local/bin/nanobot gateway`, `Environment=` řádky odkomentované, `PATH` opraven na adresář. Wrapper `~/bin/nanobot.sh` smazán. *(Taky provedl uživatel.)* Wrapper měl tutéž chybu (`$HOME/.local/bin/uv`) a i kdyby byl správný, na `exec` by nedosáhl — ale `Environment=PATH=` v unitu **zůstává**, protože `run_cli_app` spouští CLI appky s `env=os.environ.copy()` (`apps/cli/service.py:1372`).
- `workspace/AGENTS.md`: přeneseny dvě unikátní sekce z `TOOLS.md` — `python — use uv` a `Doručené připomínky` (při přenosu opraveno `remind_edit.py` → `remind_cli.py` po přejmenování z 2026-06-10). Sekce `exec`/`grep`/`cron` zahozeny jako duplicita `tool_contract.md`.
- `workspace/TOOLS.md` smazán na serveru i v repu (šablona ho neobnoví — v `nanobot/templates/` už není).
- Restart přes `~/bin/restart.sh` v 07:05:20.
**Ověření (vše zelené):**
- Reprodukce exec prostředí: `uv → /home/nanobot/.local/bin/uv`, `node`/`npm` z nvm.
- Reálný běh `uv run skills/remind/scripts/remind_cli.py list` v simulovaném prostředí → exit 0, výpis reminderů.
- **Ostrý test přes agenta** v izolované session `cli:uv-verify-20260801` (`nanobot agent -m …`): agent zavolal bare `uv run skills/remind/scripts/remind_cli.py list`, **jedno volání, exit OK, 1664 B**, žádný fallback na absolutní cestu, žádných 127.
- `maxMessages` warning: **0 výskytů** od restartu (před restartem 145/h). Journal jinak bez chyb.
- MD5 `AGENTS.md` shodné server ↔ repo, vlastník `nanobot:nanobot`, config práva `600`.
- Doručování reminderů nedotčené (crontab má vlastní PATH): `reminder_cron.log` má mtime **2026-07-15**, tj. sender od té doby neprodukoval výstup ~23 000 běhů.
**Vedlejší nález (neopraveno, mimo zadání):** v `reminder_cron.log` je z 2026-07-15 traceback `sqlite3.OperationalError: cannot rollback - no transaction is active` na `remind_send.py:182` — `conn.execute("ROLLBACK")` na neaktivní transakci **maskuje původní chybu** (tam šlo o výpadek DNS při doručování). Latentní, od 07-15 se neopakoval. Kandidát na `todo.md`.
**Co zbývá:** Zvážit návrh do `decisions.md` (čeká na formulaci autora): (1) PATH pro `exec` se konfiguruje výhradně přes `tools.exec.pathPrepend`, ne přes systemd unit ani `allowedEnvKeys`; (2) systemd unit spouští binárku přímo, bez shell wrapperu. Testovací session `sessions/Y2xpOnV2LXZlcmlmeS0yMDI2MDgwMQ.jsonl` ponechána jako doklad. Repo kopie `workspace/original/` (referenční upstream šablony) nesahané — pořád obsahují starý `TOOLS.md`.
**Jak vrátit zpět:** `cp ~/.nanobot/backup/config.json.bak-20260801-070420 ~/.nanobot/config.json` + `systemctl --user restart nanobot`; `git checkout fc45e19 -- workspace/` + `rsync -av workspace/AGENTS.md workspace/TOOLS.md nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/`. Unit se dá vrátit z tvaru zaznamenaného výše. Nic se nemazalo nenávratně — `TOOLS.md` je ve verzované historii repa (commit `fc45e19`).
## 2026-09-01 06:45 — Skill `reflect`: sebe-diagnostika ze `sessions/` se schvalovací smyčkou
**Cíl:** Zrealizovat plán `plans/reflect-skill.md` — vytěžit 534 nikdy nečtených session logů (16 MB) na opakující se chyby agenta, nálezy předkládat uživateli po jednom ke schválení a aplikovat jen schválené. Klíčový požadavek uživatele: diagnózu musí dělat LLM běžící na nanobotu (jinak to nemá smysl), nic se nesmí aplikovat automaticky, Telegram jen notifikuje.
**Co jsem zkusil / co fungovalo a proč:**
*Změřená východiska.* Z 534 session je 332 reálných konverzací (12 311 zpráv). **89 % objemu (12,5 z 14,1 MB) tvoří návratové hodnoty toolů** — pro diagnózu bezcenné, stačí `name(args) → ok|ERROR, velikost`. Přírůstek 4,4 session/den, aktivita jen 70 % dní. Preset `glm53` má reálně 976k okno (`contextWindowTokens` presetu přebíjí `defaults` 65536, `agent/loop.py:476`), input budget ~958k tokenů.
*Architektura — tři skripty, tvrdě oddělené role.* `reflect_distill.py` dělá **jen mechanickou destilaci** (filtr šumu podle prefixů, ořez tool výsledků); žádné detektory, protože chyby má hledat LLM. `reflect_auto.py` je cron runner podle vzoru `compact_memory_auto.py` (fresh `session_key`, Telegram přímo přes Bot API, zpráva se skládá ve skriptu) a **nemá v sobě žádnou cestu k zápisu do cizího souboru**. `reflect_apply.py` je jediné místo, kudy vede editace.
*Aplikační brána přesunuta z promptu do kódu.* Původně měl patch aplikovat agent podle postupu v `SKILL.md`. Přepsal jsem to na skript — recept z `develop/history.md` říká, že měkká instrukce nestačí, musí to být tvrdá brána. `reflect_apply.py` odmítne patch, jehož `old_text` v souboru není nebo je tam vícekrát, odmítne nález, který není `open`, commituje výhradně `git add -- <file>` a rozdělaný cílový soubor nejdřív checkpointne. 96 testů, včetně reálného `git revert`.
*Práh notifikace.* Denní dávka (medián 4 session) je moc malá na vzor. Nález poprvé → `watch`, mlčí; při opakování (≥2× a ≥2 session) → `open` + Telegram; zamítnutý vzor se **už nikdy neotevře** (zamítnutí je rozhodnutí, ne odklad); vzor po `applied` → `open` s příznakem regrese.
*Guard proti zápisu při analýze.* Před a po agentním tahu se porovná `git status --porcelain` + HEAD celého workspace. Kdyby agent při analýze cokoli zapsal, nálezy se zahodí.
**Ověřeno ostrým během (15 session, 90 kB destilátu):** model našel 8 vzorů, všechny konkrétní a doložené citacemi session — mj. `answer-self-config-from-guesswork` (vymyšlené schéma `tools.my.allow_set` zapsané do živého configu), `tool-call-leaked-as-text` (surový tool call se speciálními tokeny jako poslední zpráva session), `retry-without-diagnosis` (18× ve 4 session, vč. rozbité dvojité URL `r.jina.ai/http://r.jina.ai/http://…`). Dva běhy po sobě → **8 nálezů bez jediné duplicity**, dedup přes `pattern` funguje. `--check` na reálném patchi do `SOUL.md` prošel a soubor nezměnil. Sebe-vyloučení ověřeno na reálné `reflect:*` session.
**Dvě chyby, které běh odhalil, a jejich fix:**
1. `uv` **není v `PATH` neinteraktivního SSH** — první běh spadl na `timeout: failed to run command 'uv'`. Fix: plná cesta `/home/nanobot/.local/bin/uv`. Crontab má vlastní `PATH`, tam stačí `uv`.
2. **Model rozbil JSON českou uvozovkou** — napsal `(„repeated external lookup blocked")`, kde zavírací uvozovka je ASCII `"`, což předčasně ukončí JSON string. Hláška „no parseable json block" navíc retry nedala nic použitelného. Fix: validátor hlásí řádek, sloupec a výřez okolo chyby + prompt zakazuje uvozovky uvnitř string hodnot. Po opravě prošel **první pokus**.
3. Odhad tokenizace byl mimo: naměřeno **1,2 znaku na token** (ne 3), takže dávka 800 kB by dala ~670k tokenů. Sníženo na 500 kB.
**Nasazeno:** `skills/reflect/` na server (`nanobot:nanobot`), crontab `30 3 * * *`. Stav žije v `workspace/reflect/` (mimo adresář skillu, aby ho `rsync` nepřepsal) a je verzovaný v git workspace.
**Co zbývá:**
- **Uživatel projde prvních 8 nálezů přes `/reflect`** — to je zpětná vazba na kvalitu. Backlog (zbývá ~405 session, cursor stojí na `2026-05-27 14:39`) jsem **záměrně nepouštěl celý**: nemá smysl generovat desítky nálezů, dokud se neověří, že jsou k něčemu.
- Workspace na serveru má 11 nezacommitovaných změn (`cron/jobs.json`, `memory/*`, …) — před ostrým používáním uklidit, jinak bude diff po patchi zašuměný.
- Po každé review dotáhnout změněné soubory ze serveru do repa (obě git repa jsou bez remote, nespojená).
- Návrh do `decisions.md` (čeká na formulaci autora): (1) sebemodifikace jen přes `reflect_apply.py` po explicitním schválení jednoho nálezu, nikdy agentem přímo; (2) skript destiluje, quality judgements dělá výhradně LLM.
**Jak vrátit zpět:** `crontab -e` a smazat dva řádky `reflect`; `rm -rf ~/.nanobot/workspace/skills/reflect ~/.nanobot/workspace/reflect ~/.nanobot/workspace/results/2026-09-01_reflect.md`; lokálně `git revert` commitů `4aff500`, `1a8bc2a`, `7780841`, `ecc7d03`, `1bb5f4d`. Nic destruktivního neproběhlo — skill zatím jen četl a zapisoval do vlastního sandboxu.
## 2026-09-02 06:05 — skill `plan`: odstranění češtiny ze `SKILL.md`
**Cíl:** V `skills/plan/SKILL.md` nesmí zůstat jediné české slovo — konvence projektu je, že skilly pro nanobota jsou EN-only.
**Co jsem zkusil:** Nejdřív diff serverové verze proti lokální (`ssh cat` + `diff`) — byly **identické**, takže se nic ze serveru nezahazovalo. Pak dvě chirurgické editace: (1) nadpisy v šabloně plánu `## Kontext / ## Postup / ## Ověření` → `## Context / ## Steps / ## Verification`; (2) český literál schvalovací otázky (`Plán uložen do … Schvaluješ? Mám ho vykonat teď?`) nahrazen anglickou **instrukcí** místo doslovného textu — lokalizaci už pokrývá stávající pravidlo *"Respond in the user's language"*.
**Co fungovalo a proč:** `grep -nP '[ěščřžýáíéúůťďň…]|Kontext|Postup|Schvaluj|teď'` vrací prázdno. Nasazeno `rsync`em do `~/.nanobot/workspace/skills/plan/`, vlastník `nanobot:nanobot`, po nasazení server ↔ lokál `IDENTICAL`. Restart služby nebyl potřeba — skilly se čtou při každém tahu.
**Důsledek:** Nanobot bude nově psát plány do `workspace/plans/` s anglickými nadpisy i pro česky mluvícího uživatele. Tělo plánu si model dál lokalizuje, mění se jen struktura.
**Jak vrátit zpět:** `git revert` commitu se změnou skillu + znovu `rsync` na server.
## 2026-09-02 06:10 — skill `project`: deterministický zápis do `memory.md`, přepis skillu, oprava dat
**Cíl:** Review skillu `/project` proti reálným datům, která pod ním na serveru vznikla (`projects/{chata,life,proxmox,radio1}`), a oprava toho, co v praxi nedrží.
**Co review našlo (doloženo v datech, ne odvozeno z textu skillu):**
1. **Vymyšlená data** — `chata/memory.md` měl dva záznamy datované `2026-09-14`, přitom byly zapsány 2026-09-01 (mtime 10:51). Skill předepisoval formát `- YYYY-MM-DD:`, ale neříkal, odkud datum vzít — a model má přitom `Current Time` v runtime kontextu.
2. **Slepený řádek** — `proxmox/memory.md` obsahoval `…jako další VM/kontejner.- 2026-09-01: Rozhodnuto —` na jednom řádku. `chata/memory.md` neměl koncový newline.
3. **`state.md` se nikdy nezaložil** — `chata` 0 B při 2 KB memory. Pravidlo *„offer to draft it"* je příliš měkké a nespustí se.
4. **Mrtvé pravidlo o Dreamu** — *„Dream must not touch `workspace/projects/`"* žilo v těle skillu, které Dream nikdy nečte, a upstream to už vynucuje kódem: `build_dream_tools()` (`nanobot/agent/memory.py:641`) dává Dreamu Write/Edit/ApplyPatch s `allowed_dir = workspace/skills` + tři memory soubory. Do `projects/` se nedostane. **Nekryté je riziko opačné** — Dream smí přepisovat samotný skill, a serverový `SKILL.md` byl 2026-09-01 13:15 skutečně změněn mimo repo.
**Co jsem zkusil a co fungovalo:**
- **Nejdřív dotažen server → repo** (commit `3c388ba`), aby byl přepis čitelný v diffu.
- **`skills/project/scripts/project_cli.py`** (`activate` / `log` / `list` / `new`) přebírá datum ze systémových hodin a newline hygienu. Text jde na **stdin quoted heredocem** (`<<'NOTE'`) — shell obsah neinterpretuje, takže `„"`, `'` i `"` projdou doslova. Ověřeno reálným zápisem.
- **16 pytest testů** lokálně zelených; na serveru se testy nespouští (konvence).
- **Data opravena** po odsouhlasení uživatelem: data `09-14` → `09-01`, rozdělen slepený řádek, doplněn newline, `chata/state.md` sepsán z historie (dřevo + otevřená otázka zazimování). Záznam o zazimování zůstal i v `memory.md` — append-only invariant se neporušil.
**Rozpočet velikosti — návrh, který padl:** Původně jsem navrhoval prahy 8 000 / 12 000 znaků odvozené z okna 65 536 tokenů. **Bylo to postavené na špatném čísle:** `agents.defaults.contextWindowTokens: 65536` preset přebíjí a default `glm53` má 976 000. `proxmox` (11 278 znaků ≈ 9 400 tokenů při 1,2 znaku/token) tedy zabírá ~1 % okna. Uživatel návrh zamítl s tím, že projekt nemá ztrácet zadaná data — správně. Zůstalo jediné reálné omezení `maxToolResultChars: 16000`, které je ale **omezením čtení, ne ukládání**: `activate` nad ním vypustí z *výstupu* nejstarší záznamy a ukáže cestu k plnému logu; soubor na disku se nemění (ověřeno na 33 790znakovém souboru — výstup 14 305 znaků, velikost souboru beze změny).
**Chyba v mém vlastním review:** tvrdil jsem, že `chata/memory.md` má „4 záznamy na 3 řádcích". `wc -l` počítá newliny, ne řádky — soubor měl 4 řádky a jen mu chyběl koncový newline. Slepený řádek byl reálně jen v `proxmox`.
**Co zbývá:**
- Reálný smoke test přes Telegram/WebUI (aktivace projektu, zápis poznámky s dnešním datem, „vypiš projekty", dotaz na detail z `chata`).
- Návrh do `decisions.md` (čeká na formulaci autora): (1) zápis do `memory.md` výhradně přes `project_cli.py log`; (2) projektová data nemají strop ani konsolidaci — velikost se řeší jen na straně čtení; (3) `artifacts/` vzniká líně při prvním artefaktu.
- `radio1` (prázdné `memory.md`/`state.md` od 23. 7.) — uživatel rozhodl nechat být.
**Jak vrátit zpět:** Zálohy dat jsou na serveru v `workspace/backup/projects-2026-09-02/` (`chata-memory.md`, `chata-state.md`, `proxmox-memory.md`) — `cp` zpět na místo. Skill: `git revert 39b3fb3 3c388ba` + `rsync` na server. Restart služby netřeba, skilly se čtou při každém tahu.
## 2026-09-02 09:05 — reflect: klouzavé okno, audit počtů a záznam rozhodnutí
**Cíl.** Uživatel se zeptal, jestli jsou nálezy z dávkované analýzy věrohodné a jak by se
změnily nad celou historií. Kontrola provozu odhalila tři nezávislé problémy plus jeden,
na který se doptal později (záznam rozhodnutí).
**Co jsem zjistil (data z běhů 1.2. 9.).**
- `cursor: 2026-05-29`, session sahají do `2026-09-02` → 3 běhy × 1 dávka = **56 z 356
session (16 %)**, a všechny z nejstarších 4 dnů korpusu. Backlog 19 dávek při ~1 dávce/noc
neklesal. Všech 8 rozhodnutých nálezů tedy popisovalo chování z konce května.
- Dávkování samo nálezy nezkreslilo — každý běh byl **jedna** dávka. Zkreslilo je okno.
- `f09a7`: `occurrences: 4`, `sessions_affected: 5` — aritmeticky nemožné. Počty jsou
nekontrolované self-reporty modelu a `merge_findings` je jen sčítá.
- `ff77b`: tvrdí 18× a nese 2 důkazy — fold přepisoval `evidence`, ale `occurrences` sčítal.
- Audit zaznamenával **výsledek, ne rozhodnutí**: u zamítnutí chybí důvod (4 z 8 zamítnuto),
`uprav:` přepsalo `patch.new_text` a zahodilo návrh modelu, `přeskoč` nezanechalo nic.
**Co jsem zkusil a co funguje.**
1. **Klouzavé okno** (`--window-days`, default 21): `since = max(cursor, now - okno)`.
Cursor je podlaha (nic dvakrát), okno strop (starý backlog se přeskočí natrvalo).
Backlog se nedohání — rozhodnutí uživatele, archeologie z května hodnotu nemá.
2. **Ostrý `--all` odmítnut** — znovu čte spočítané session a `merge_findings` jim sečte
`occurrences` do existujícího záznamu. Je to nafukovač počtů; zůstává pro `--dry-run`.
3. **Clamp počtů** ve validátoru: `sessions_affected ≤ min(occurrences, session_count)`.
Clamp, ne odmítnutí — retry stojí celý tah.
4. **`_fold_evidence()`** — při foldu se důkazy kumulují (nejnovější první, dedup, cap 6),
takže kumulativní počet jde ověřit. U regrese se nekumulují (mísily by před/po opravou).
5. **Prompt**: model dostal informaci, že vidí jen výsek, a smí hlásit i jediný výskyt
**známého** vzoru. Tím se zavřela slepá skvrna — vzor s frekvencí ~1×/dávka se dřív
nikdy nepojmenoval (prompt to zakazoval) → nikdy nespočítal → práh nepřelezl.
6. **Záznam rozhodnutí** v `reflect_apply.py`: `--reject` vyžaduje `--reason` (povinně —
je to jediná zpětná vazba na kvalitu analýzy), nová akce `--skip` s počítadlem,
`patch` zůstává návrhem modelu a uživatelova verze jde do `applied.new_text`
(`APPLIED-EDITED` v logu). `SKILL.md` má sekci *Decision history*.
7. **Report a Telegram** nesou okno a nemlčí o nezpracovaném zbytku — dřív se při 0 nálezech
vracel prázdný string, což je přesně důvod, proč hladovění cursoru tři noci nikdo neviděl.
Testy: 140 prošlo (`tests/`), ruff clean. Nasazeno rsyncem, vlastník `nanobot:nanobot`.
**Ostrý běh 09:0809:31 — ověřeno na živých datech.**
```text
Zpracováno 74 session ve 6 dávkách. Nálezů: 29 (23 k review, 6 sledovaných).
Okno: od 2026-08-12, dávek 6/6.
Známé vzory: 100,0 výskytu / 100 session (minule 57,7).
```
- Okno v hlavičce reportu i ve `state.json` (`window_from`, `batches_total`), cursor
přeskočil z `2026-05-29` na `2026-09-02T06:22` — backlog je minutý, jak bylo rozhodnuto.
- **Kumulace důkazů funguje**: `f2b3d retry-without-diagnosis` má 67 výskytů a nese 6 důkazů
(cap `MAX_EVIDENCE`) posbíraných napříč dávkami; `f999d` z 2. dávky se do něj složil.
- **Žádný nový nález není nekoherentní** (`sessions_affected ≤ occurrences` všude).
Jediný nekoherentní záznam ve store je starý `f09a7` (4×/5 session) — clamp platí na
nově parsované nálezy, historii zpětně nepřepisuje.
- Guardy `reflect_apply.py` odmítly `--reject` bez `--reason`, `--reason` bez `--reject`
i `--skip --reject`, aniž by na store sáhly.
**Korekce mé vlastní obavy z dimenzování.** Z dry-runu jsem usoudil, že 6 dávek se do
20min deadline nevejde. Neplatí: dávka trvá **~4 min**, celé okno 23 min, a deadline pustil
i šestou. Je to ale těsné (kontrola deadlinu padla na ~20 min u indexu 5), takže jedna
pomalejší dávka běh zastaví a zbytek nechá na další noc — což Telegram ohlásí. Zvýšení
`DEFAULT_DEADLINE_MINUTES` na ~40 min je proto drobná pojistka, ne nutnost.
**Co zbývá.** 23 open nálezů k review a **ani jeden nemá patch** — potvrzuje otevřenou
položku o stropu „read at most 2 files". Regrese na session starších než oprava z 1. 9.
se pořád hlásí falešně (okno začíná 12. 8., patche jsou z 1. 9.).
**Jak to vrátit zpět.** Skripty: `git revert` v tomto repu + rsync. Stav na serveru:
`/tmp/state.json.bak` a `/tmp/findings.jsonl.bak` (záloha před ostrým během).
## 2026-09-02 10:05 — reflect: celý skill do angličtiny
**Cíl.** `SKILL.md` a Python skripty skillu `reflect` mají být EN-only (konvence
z `CLAUDE.md`), včetně textů, které skript generuje.
**Co jsem zkusil.**
- `SKILL.md`: ukázkový výstup nálezu, tabulka rozhodnutí a odkazy na ně v textu byly česky.
Klíčová slova v tabulce jsou teď `ok` / `apply`, `edit:`, `skip`, `reject`, `stop`
+ věta, že se přijímají ekvivalenty v jazyce, kterým uživatel píše. Pod ukázkou přibyla
věta, že popisky se píšou v jazyce uživatele.
- `reflect_auto.py`: report (`Zpracováno…`, `Okno:`, `Známé vzory:`, `**Výskyt/Důkazy/Návrh**`,
`REGRESE`, `poprvé/naposledy`, `Nic k hlášení`), Telegram hlášky (`nálezů k review`,
`Napiš /reflect`, timeout) a `ReflectError("model nedostupný")` → anglicky.
- `reflect_distill.py`: hlavička destilátu (`N zpráv`) a řádek statistik do stderr.
- Testy: asserty na tyto texty + česká fixture data. Ponechán jen znak `` v testu
neescapovaných uvozovek — to je předmět testu, ne jazyk.
- Vedlejší úklid: `_decimal()` existovala jen kvůli desetinné čárce; v anglickém reportu
by `3,2 occurrences` četlo špatně, takže tečka a helper inlinovaný do `_rate_line`.
**Co fungovalo a proč.** `pytest tests -q` → **140 passed**. Před nasazením ověřeno, že
server == HEAD u `SKILL.md`, `README.md` i všech tří skriptů (nikdo do nich mimo nás
nesáhl), pak `rsync` celého adresáře, vlastník `nanobot:nanobot` sedí. Workspace na serveru
jsem necommitoval — je trvale dirty, jak je tam zvykem.
**Co zbývá.** `README.md` zůstává česky — je to dokumentace psaná uživateli, mimo dohodnutý
rozsah („SKILL.md a python skripty"). Nejbližší noční běh pošle Telegram i report anglicky.
**Jak to vrátit zpět.** `git revert 2375d76` + rsync skillu na server.
## 2026-09-02 10:15 — reflect: druhé kolo oprav (set-patch, dedup, hradlo na regrese)
**Cíl.** Vykonat [plans/reflect-verohodnost.md](plans/reflect-verohodnost.md): složení
patche při review má být jedno validované volání skriptu, počty v reportu mají odpovídat
store, a příznak regrese má znamenat „vrátilo se to **po** opravě".
**Co jsem zkusil.**
- `reflect_apply.py`: nová akce `--set-patch <json>` (`{file, old_text, new_text}`).
Pořadí je celý smysl: kandidát `{**record, "patch": novy}` projde existující
`check_patch()` (status `open`, cesta uvnitř workspace, právě jeden výskyt `old_text`,
„nemění nic") a **teprve pak** se zapíše `patch` + `patch_drafted_at` a `DRAFTED` do
auditu. Vypíše rovnou i diff, takže nenásleduje druhé volání `--check`. Vytažen helper
`_diff()` (sdílí ho `--check`) a `_parse_patch_file()`. Přepis existujícího patche
povolen — guard je status `open`, ne absence patche.
- `reflect_auto.py` bod 2: `merged` je `dict[str, Finding]` klíčovaný `pattern` místo
seznamu, do kterého se po každé dávce přičítalo. Vzor nalezený v 5 dávkách byl v reportu
5× (report z 2. 9.: **29 nadpisů proti 13 vzorům ve store**) a stejně nafouknutý šel do
`stats` i do Telegramu.
- `reflect_auto.py` bod 3: nové derivované pole `Finding.last_seen` = `max(evidence[].when)`
přes tvary `^\d{4}-\d{2}-\d{2}` (prvních 10 znaků, nedatumové hodnoty zahozené), fallback
`created`. Dvě použití: `regression_of` se nastaví jen když `last_seen > applied.at[:10]`,
jinak `stale_after_fix` → status `watch` (ne `open`, ne REGRESE); a `_seen_line()` hlásí
`last_seen` místo `created`, takže „naposledy" přestalo být datum přepsání záznamu.
Report navíc značí `— STALE` nález, jehož `last_seen` je před `window_from` běhu.
- `SKILL.md`: do STOP gate 4 doplněn **zákaz ruční editace `reflect/findings.jsonl`**
(chybějící věta, kvůli které agent legálně sáhl do store), postup `--set-patch` místo
„propose the exact old_text/new_text yourself", řazení v *1. Load* na severity →
`last_seen` → `occurrences` (s fallbackem na `created` u starých záznamů) a značení
zastaralých nálezů. `README.md`: `DRAFTED` v tabulce rozhodnutí, co znamená „zastaralý",
regrese jen po opravě, záruka o zápisu do store.
- Testy: +26 (celkem **166 passed**, ruff čistý). Klíčové: neaplikovatelný i nejednoznačný
`--set-patch` nechá záznam **bez patche** (ověření běží před zápisem), úspěšný nezmění
cílový soubor, přepis vadného draftu (scénář `fb33c`), vzájemné vyloučení s
`--check`/`--skip`/`--reject`; dedup na úrovni běhu (dvě dávky, jeden nadpis, `open` = 1);
hradlo na regresi z obou stran; `last_seen` přes míchané tvary a fallback; STALE v reportu.
**Co fungovalo a proč.** Před nasazením ověřeno, že server == HEAD u všech 8 souborů
skillu (Dream do nich nesáhl), pak `rsync` celého adresáře. Na serveru:
`pytest tests/ -q` → **166 passed**, a ostrá zkouška hradla podle plánu — `--set-patch`
s neexistujícím `old_text` na živém nálezu `f2dd0` skončil `exit=2`
(`the original text is no longer in SOUL.md`) a záznam zůstal `patch: None`,
`patch_drafted_at: None`, status `open`. Store se neušpinil neúspěšným pokusem.
Uzavírá položku o falešných regresích z [todo.md](todo.md) — chybějící datum session, které
plán zmiňoval jako překážku, dodává `last_seen` odvozený z `evidence[].when`.
**Co zbývá.** Dvě verifikace z plánu vyžadují běh, který nejde udělat odsud: jeden
`/reflect` v chatu na nálezu bez patche (musí projít bez zápisu do `findings.jsonl` mimo
skript, `log/reflect.log` musí mít `DRAFTED`) a ostrý běh `reflect_auto.py` (report má mít
tolik nadpisů, kolik je vzorů ve store; žádný nález s výhradně před-opravným důkazem nesmí
nést REGRESE). Zbytek výčtu „Co to NEpokryje" z plánu platí dál — hlavně že analýza patche
nevrací vůbec.
**Jak to vrátit zpět.** `git revert` v tomto repu + `rsync` skillu na server. Store na
serveru změněn nebyl.
---
## 2026-09-02 10:30 — Audit AGENTS.md / SOUL.md / USER.md + čtyři cílené opravy
**Cíl.** Prověřit konzistenci a duplicity tří bootstrap souborů na serveru a opravit jen to,
kde je doložený problém.
**Co jsem zkusil.** Nejdřív analýza proti čtvrté, dosud nepočítané vrstvě promptu:
`tool_contract.md` (83 řádků) se vkládá do system promptu **každý tah, bezpodmínečně,
hned za AGENTS/SOUL/USER** — `agent/context.py:88`
(`parts.append(render_template("agent/tool_contract.md"))`), nanobot-ai 0.3.0. Proti němu
i mezi sebou porovnány všechny tři soubory.
Nasazené čtyři opravy (Python skript přes SSH, každá náhrada musela matchnout právě 1×,
jinak se nezapsalo nic):
- **SOUL.md, řádky 2122** — potvrzovací pravidlo. Bylo: „nastíň plán a *počkej na
potvrzení*" + „*před zápisem/úpravou souborů požádej o potvrzení*". Je: „nastíň plán a
**v tomtéž turnu ho rovnou začni provádět** — plán není konec turnu. Hlas průběžný status"
+ „na potvrzení čekej **jen** u nevratných akcí (smazání, odeslání ven, restart služby)".
Řádek 20 nechán beze změny.
- **AGENTS.md** — `- **TypeScript** — via bun (preferred) or deno / node if available`
→ `- **TypeScript** — always via bun`.
- **USER.md** — smazány tři řádky bez akční hodnoty: `Silver Gym`,
`Zná lidi: Horáčková, Přibyl`, a `Používá Pi agent (coding agent); AGENTS.md je jeho
primární instruction file, ne CLAUDE.md` (poslední se dal přečíst jako instrukce
o *vlastním* AGENTS.md).
- **USER.md** — `- Připomínky: stručné, přirozené zprávy…` → `- Text připomínky (při
zakládání): stručná, přirozená česká věta…`; doručení jde mimo agenta, `remind_send.py`
posílá uložený text verbatim.
**Co fungovalo a proč.** Hlavní nález: SOUL.md říkalo čekat na potvrzení po vypsání plánu,
zatímco `tool_contract.md` („outline the plan briefly **and then execute it**"), USER.md:45
i uživatelova vlastní korekce v `keep.md` říkaly opak. Ta korekce se nikdy neprojevila,
protože **`keep.md` není bootstrap soubor** (`BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md",
"USER.md"]`, `context.py:57`) — agent ho každý tah nečte. Oprava proto musela jít do SOUL.md.
Ve výčtu nevratných akcí záměrně **není „přepis souboru"** — každá editace něco přepisuje,
takže by to zadními vrátky vrátilo odstraňované pravidlo; workspace má navíc git store.
Ověření prostředí opravilo dřívější chybný závěr: `rustc`/`cargo` 1.98.0 a `bun` 1.4.0
**jsou** k dispozici, jen ne v non-login SSH PATH — `exec` tool je vidí přes
`tools.exec.pathPrepend` v `config.json`. Reálně chybí jen `deno`.
Zamítnuto (nálezy bez důkazu nebo se zásahem nad přínos): explicitní override
`tool_contract.md` u reminderů (AGENTS.md „**Never** use the `cron` tool" je konkrétnější
a žádné selhání není doložené); výjimka u terse pravidla v SOUL.md; smazání
`## Behavioral Guidelines` a zkrácení `## python — use uv` v AGENTS.md (skill `python` je
on-demand, tahle sekce je jediné trvale přítomné místo s `uv` pravidlem); přesuny řádků mezi
soubory kvůli doménové čistotě — všechny čtyři vrstvy jdou do jednoho promptu za sebou,
model nerozlišuje původ řádku.
Restart nebyl potřeba (bootstrap soubory se čtou čerstvě každý tah). Repo `workspace/`
sesynchronizováno ze serveru — bylo zastaralé o 12 měsíce, takže s opravami přišly i
autonomní změny Dreamu (SOUL.md naposledy 09:43 téhož dne).
**Co zbývá.** Verifikace vyžaduje běh v chatu: (1) vícekrokový úkol — musí začít provádět
v témže turnu; (2) drobná změna souboru — bez žádosti o potvrzení; (3) `smaž tmp/xyz.txt` —
potvrzení se **musí** zachovat; (4) „umíš deno? a rust?" — deno ne, rust ano. Pokud agent
pořád končí turn plánem, hledat v `## No proactive actions` v AGENTS.md.
Dream smí zapisovat do SOUL.md a USER.md (`memory.py:641`,
`extra_write_allowed_files`), do AGENTS.md ne — body 1, 3 a 4 tedy časem částečně vrátí zpět.
**Jak to vrátit zpět.** Zálohy na serveru:
`~/.nanobot/backup/{AGENTS,SOUL,USER}.md.bak-20260902-103002` → zkopírovat zpět do
`~/.nanobot/workspace/`. V repu `git revert` commitu s `workspace/`.
### Doplněk 2026-09-02 10:40 — výsledky verifikace
Testy spuštěny přes Python API v izolovaných sessionech (`Nanobot.run(session_key="verify:…")`),
živý chat nedotčen.
- **Test 4 (jazyky) — prošel.** „umis spustit deno? a rust?" → *„Rust: **ano** — rustc 1.98.0,
cargo 1.98.0. Deno: **ne** — není nainstalovaný."* Ověřil si to voláním, ne z paměti.
- **Test 1 (vícekrokový úkol) — prošel.** „spocitej kolik radku ma kazdy .md soubor
v knowledge/ a uloz souhrn do tmp/verify_lines.md" → provedl v témže turnu, neskončil
plánem, soubor reálně vznikl (217 B, obsah odpovídá). **Hlavní oprava funguje.**
- **Test 3 (potvrzení u smazání) — NEPROŠEL.** „smaz soubor tmp/verify_lines.md" → odpověď
*„Smazáno."*, soubor pryč, bez žádosti o potvrzení.
**A/B ověření příčiny:** stejný prompt spuštěn proti záloze původní SOUL.md
(`SOUL.md.bak-20260902-103002`, řádek 22 „u nevratných akcí (smazání, odeslání) **vždy**
čekej na potvrzení") — chování **identické**, také rovnou smazal. Swap byl atomický
s `trap` na obnovení; server i repo po testu na `md5 da10595f`.
Závěr: **není to regrese z této změny.** Pravidlo o potvrzení u mazání nefungovalo ani
před ní. Pravděpodobná příčina je `tool_contract.md` („Treat a clear user request as
authorization to complete it in the current turn") ve spojení s přímým imperativem na
konkrétní pojmenovaný soubor — což je nejspíš správné chování a **chybné bylo kritérium
testu**, ne agent. Otevřená otázka je, jestli se guard chová stejně u odvozeného mazání
(„ukliď tmp/", „smaž staré remindery"), kde už autorizace jednoznačná není. Netestováno.
Bez doloženého problému se do SOUL.md dál nesahá — v souladu s disciplínou plánu.
**Vedlejší nález (nesouvisí s touto změnou):** do `tmp/verify_lines.md` agent zapsal
`Vytvořeno: 2026-06-13`, ačkoli runtime context nese `Current Time` 2026-09-02.
Vymyšlené datum proti explicitním antihalucinačním pravidlům v SOUL.md.
## 2026-09-02 12:20 — `/reflect`: diff a odhad šance hned u každého nálezu
**Cíl:** Uživatel rozhodoval nad větou (`Proposal: přidat STOP gate do sekce Fetching`),
ne nad reálnou změnou souboru, a bez vodítka, jestli taková úprava chování vůbec zastaví.
Chtěl u každého předloženého nálezu vidět **hotový diff** a **odhad v procentech**.
**Co jsem zkusil:**
- **Draft patche před prezentací, ne na vyžádání.** Nový krok `2. Prepare the patch`
v `SKILL.md`: nález s patchem → `--check`; bez patche (dnes všech 7 otevřených) → složit
hned přes `--set-patch`; nález, který editací souboru opravit nejde → říct to a nabídnout
jen skip/reject, patch nevymýšlet. Kroky se posunuly na `1..6`, odkazy uvnitř souboru
srovnány.
- **Zamítnuto: `--set-patch --dry-run`.** Zvažoval jsem režim, který diff ukáže bez zápisu
do store. `--set-patch` patch ověřuje *před* uložením, takže nepoužitelný pokus se do
`findings.jsonl` nedostane tak jako tak — jediné, co by dry-run ušetřil, je řádek
`DRAFTED` u později zamítnutého nálezu. Ten má ale cenu: „tohle konkrétní řešení jsme ti
ukázali a tys ho i tak zamítl" je silnější zpětná vazba na analýzu než holé `REJECTED`.
**Žádná změna Pythonu**, jen prompty.
- **Odhad šance jako rubrika, ne volný tip.** Nová sekce `## Estimating the odds`: čtyři
pásma (~80 / ~60 / ~40 / ~20 %) podle toho, co patch mění, hlavní osa = **je opravovaný
text v kontextu ve chvíli, kdy chyba vzniká?**. O pásmo dolů za `regression_of` a za
důkazy z nesouvisejících kontextů. Zastaralý nález odhad nedostane vůbec. Povinná věta
„proč to číslo" a explicitně: odhad z toho, co je na záznamu, ne měření.
- Zamítnuto i pole `confidence` z `reflect_auto.py` — model ho vydává, když ještě žádný
patch neexistuje, takže by hodnotil větu `proposal`, ne to, co se do souboru zapíše.
- `README.md`: přepsaný odstavec o skládání patche + nová sekce „Šance, že oprava zabere".
**Co fungovalo a proč:**
- Nasazeno rsyncem (jen `SKILL.md` + `README.md`, owner `nanobot:nanobot`), **166 testů
na serveru prošlo** — kód se nezměnil, testy byly regresní pojistka.
- **Ověřeno na reálném nálezu bez zásahu do ostrého store:** `reflect_apply.py` má
`--workspace`, takže jsem v `tmp/reflect_probe/` (kopie `findings.jsonl` + `SOUL.md`)
složil patch pro `f5c34 answer-self-config-from-guesswork` a spustil `--set-patch`.
Výsledek: exit 0, skript vytiskl diff, v probe store přibyl `patch` +
`patch_drafted_at`, status zůstal `open`, audit dostal `DRAFTED f5c34 … SOUL.md` —
a **ostrý `findings.jsonl` i `SOUL.md` zůstaly nedotčené**. Probe smazán.
- Aplikace rubriky na ten samý nález: patch přidává tvrdý zákaz do `SOUL.md` (vždy
v kontextu) → ~60 %, ale `regression_of: fbb44` → o pásmo dolů → **~40 %**.
**Co zbývá:** Reálný `/reflect` v chatu — ze SSH ho spustit nejde. Kritérium: v jedné
odpovědi diagnóza, důkazy, ověřený diff a řádek s pásmem + větou proč, aniž by si o diff
kdokoli říkal; po `přeskoč` musí být cílový soubor nedotčený a v `log/reflect.log` řádky
`DRAFTED` + `SKIPPED ×1`. Splývá to s už otevřenou položkou v `todo.md` („Doověřit druhé
kolo oprav `reflect`") — ověřit obojí jedním sezením.
## 2026-09-02 13:55 — reflect: deep review, regrese před závažností a zkrácení `SKILL.md`
**Cíl:** Uživatel se zeptal, jestli není skill `reflect` překombinovaný a dělá-li opravdu
to, co má — deep review s návrhem, co zlepšit a co zestručnit. Podklady: lokální repo,
serverový stav (`findings.jsonl`, `state.json`, `log/reflect.log`, poslední report)
a `history.md`.
**Co review našlo:**
*Skill funguje.* V provozu prošlo 14 rozhodnutí (6× APPLIED, 3× DRAFTED, 5× REJECTED,
všechna s důvodem), hradlo `reflect_apply.py` drží — ani jeden zápis do `findings.jsonl`
mimo skript. Mrtvá mechanika se nenašla; rubrika odhadu šance z 12:20 reálně rozhodla
hned napoprvé (`REJECTED f5c34 — úspěšnost ~40 % není dostatečná…`). **Nedoověřená
položka z `todo.md` je z větší části splněná**: sekvence `12:33 DRAFTED f4ae4 → 12:34
APPLIED` a `12:35 DRAFTED f5c34 → 12:53 REJECTED` je ten reálný `/reflect` v chatu.
Čtyři věcné vady:
1. **Řazení stálo na nestabilním čísle.** `severity` přiděluje model znovu každý běh
a mezi 1. a 2. 9. se rozházela u **všech pěti** sledovaných vzorů (`answer-self-config`
high→medium, `unverified-success-claim` high→medium, `reimplement-without-checking`
high→medium, `retry-without-diagnosis` medium→low). Důsledek: `f2b3d
retry-without-diagnosis` — 67× v 7 session, **regrese** už jednou aplikované opravy
`ff77b` — dostal `low` a v pořadí seděl **poslední z pěti**.
2. **„Nález má patch" už neznamenalo „patch napsala analýza."** Od `aa6f43f` může patch
pocházet i z předchozího review (`patch_drafted_at`). Živý případ `f7575`: draft
z 12:54 a za ním v logu **žádné rozhodnutí**.
3. **Formát `history` nebyl zdokumentovaný** — hodnota je `"<created>:<id>"`, skript si ji
ořízne, agent by ji vytiskl celou.
4. Drobnosti: rubrika odhadu neměla dno pod ~20 %, `results/<date>_reflect.md` byl
v tabulce Data, ale procedura ho nikdy nepoužila, a `uv` se volalo dvěma způsoby
bez vysvětlení.
Redundance: „needituj sám, jen přes skript" bylo v `SKILL.md` **5×**, sémantika odmítnutí
skriptu 4×, display ID vs. interní `id` 3×, slovník rozhodnutí 2× (krok 4 význam, krok 5
flagy — dvě místa, která se mohla rozejít).
**Co jsem udělal:**
- **`reflect_auto.py`** — jediná změna chování: nový `_report_order()`, řadí
`(status != open, regression_of is None, -severity)`. Regrese jdou před závažnost,
protože `regression_of` je fakt z auditu, kdežto severity je per-run odhad modelu.
- **`test_reflect_auto.py`** — nový `test_a_regression_outranks_a_higher_severity_finding`.
Ověřeno, že **před** opravou padá (`assert 521 < 212`) a po ní prochází.
- **`SKILL.md`** — všechny čtyři vady opravené; krok 1 řadí regrese první,
krok 2 rozlišuje draft z dřívějšího review, sekce Data dokumentuje `history`.
Zkrácení: kroky 4 a 5 sloučené do jedné tabulky `Input | Meaning | Flags`
(význam a flag už nežijí odděleně), „needituj sám" zůstalo jen v gate 4,
výčet schématu nálezu a varování o oknech zkomprimované. **254 → 237 řádků.**
- **`README.md`** — jedna věta o novém pořadí a proč.
**Zamítnuto:** (a) *stabilizace severity při mergi* (`max(předchozí, nová)`) — znamenala
by, že severity už nikdy neklesne, i když se vzor reálně zmírní; regrese v pořadí řeší
konkrétní problém levněji. (b) *Explicitní pokyn „piš anglicky" do analytického promptu* —
uživatel ho nechtěl. Ověřeno, že **Python už 100 % anglicky je** (nula znaků s českou
diakritikou ve `scripts/` i `tests/`, převod `2375d76`); míchanice CZ/EN v reportu
z 2026-09-02 pochází z běhu v 09:31, tedy **před** tím převodem. Zbytkový zdroj češtiny
je jen model sám (destilát session je česky) a bez toho pokynu to nejde změnit.
**Co fungovalo a proč:**
- **167 testů** (166 + 1 nový) prošlo lokálně i na serveru.
- **Server neměl drift** — `rsync --checksum -n` před nasazením vypsal přesně a jen ty
čtyři soubory, které jsem změnil. Nasazeno, vlastník `nanobot:nanobot`.
- **Ověřeno na živém store, read-only:** nové pořadí nad pěti `open` nálezy dá
`[1/5] unverified-success-claim (REGRESSION)`, `[2/5] retry-without-diagnosis low 67×/7s
(REGRESSION)`, `[3/5] skill-doc-absolute-path-triggers-guard [patch drafted 12:54]`.
Před opravou byl `retry-without-diagnosis` pátý. `findings.jsonl` zůstal nedotčený.
- Zkrácení na ~200 řádků, které jsem odhadoval v plánu, nevyšlo — 237 je poctivé dno,
níž už by se řezalo do pojistek. Čistého textu ubylo ~27 řádků, ~10 přibylo novým
obsahem (vysvětlení `uv`, formát `history`, `patch_drafted_at`, dno rubriky).
**Co zbývá:** Reálný `/reflect` v chatu (ze SSH nejde). Kritéria: `retry-without-diagnosis`
se musí předložit jako **[2/5]**, ne poslední; `f7575` se musí ohlásit jako nález, jehož
patch složilo **dřívější review, které skončilo bez rozhodnutí**; řádek „first seen"
nesmí obsahovat dvojtečku ani interní id. Při té příležitosti zavřít i větev `přeskoč` —
**`SKIPPED` v produkčním logu dosud není ani jednou**.
**Jak vrátit zpět:** `git revert aef90f3`, pak `rsync -av --exclude '__pycache__'
--exclude '.pytest_cache' skills/reflect/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/reflect/`.
Nic destruktivního — změnil se jen skill, žádný nález ani store.
## 2026-09-02 14:15 — reflect: vyhozeno vysvětlování `uv` ze `SKILL.md` (korekce předchozího zápisu)
**Cíl:** Uživatel se ptal, proč jsem v předchozím kole (`aef90f3`) do STOP gate 1
v `skills/reflect/SKILL.md` vysvětloval, proč se `uv` volá plnou cestou — konvence `uv`
je přece v system promptu. Měl pravdu.
**Proč to tam nepatřilo:**
- Je to **environmentální meta-znalost**, ne pravidlo skillu. `AGENTS.md` má celou sekci
`## python — use uv` (`Run a script: uv run script.py`, `Script with declared
dependencies: uv run --script script.py`).
- Druhá polovina té věty („inside a turn plain `uv run` works") **jen opakovala, co už
`AGENTS.md` agentovi říká**.
- Vysvětlení pro člověka **už existovalo** v `README.md` skillu („Plná cesta k `uv` je tu
proto, že v neinteraktivním SSH není v `PATH`. Crontab si `PATH` nastavuje sám.") —
README se do kontextu nenačítá, takže je to správné místo.
**Co jsem udělal:** revert té jedné půlvěty, `SKILL.md` 237 → 236 řádků. Nic jiného —
Python, testy ani `README.md` se nedotkly. Nasazeno, 167 testů dál prochází, vlastník
`nanobot:nanobot`.
**Co zůstalo a proč:** *příkaz* s plnou cestou v gate 1 zůstává. Předává se **člověku**
do SSH a tam bare `uv` opravdu nefunguje — ověřeno `ssh nanobot@nanobot.hell 'command -v
uv'` → exit 1 (`PATH=/usr/local/bin:/usr/bin:/bin:/usr/games`), zatímco `bash -lc` ho
najde. Zkrátit ho kvůli konzistenci s `AGENTS.md` by uživateli dalo rozbitý příkaz.
**Zamítnuto:** přesun toho faktu do `AGENTS.md`. Agent ho nepotřebuje — v jeho `exec`
bare `uv run` funguje (doloženo šesti úspěšnými běhy `reflect_apply.py` v
`log/reflect.log`). Relevantní je jen pro člověka v SSH a tam už to `README.md` říká;
v system promptu by to byl šum za nulový přínos.
**Kontrola zbytku `SKILL.md`** na stejný typ duplicity nic dalšího nenašla: `uv run`
v krocích 2 a 4 jsou prosté příkazy, „write their version to a temp file" sedí s konvencí
`### Temporary files → tmp/` v `AGENTS.md`, a věta „Answer in the language the user writes
in" je v těle skillu **schválně** — vyžaduje to projektový `CLAUDE.md`.
**Poznámka k předchozímu zápisu:** záznam z 13:55 uvádí vysvětlení `uv` mezi opravenými
drobnostmi. `history.md` je append-only, takže se needituje — tento záznam ho koriguje.
**Jak vrátit zpět:** `git revert` tohoto commitu + rsync `SKILL.md` na server.
## 2026-09-02 14:55 — reflect: druhé zkrácení `SKILL.md` a oprava `edit:` flow
**Cíl:** deep review `skills/reflect/SKILL.md` z pohledu „jde to zkrátit se zachováním
funkcionality" — a pokud ano, přepsat.
**Co jsem zkusil:**
- Inventura po sekcích (`wc -w` na rozsahy řádků): 1 981 slov, největší bloky Odds 275,
2-Prepare 267, 4-Decide 248, Data 244.
- Porovnání s `README.md`: ranking (regrese před závažností), pásma šance, definice stale,
kumulativní počty i důvod povinného zamítnutí **už tam všechny jsou** — SKILL.md je držel
podruhé. Tři pojmy (stale, regrese, `history[0]`) byly v souboru definované 3×.
- Ověření tvrzení SKILL.md proti kódu (`reflect_apply.py`, `reflect_auto.py`).
**Co fungovalo a proč:**
- **Našel se funkční bug:** řádek `edit:` v tabulce rozhodnutí sliboval „show the new diff
and ask again", ale předepisoval `--new-text-file` bez `--check` — to jde přímo do
`apply_finding()`, tedy zápis + commit. Agent podle tabulky aplikoval uživatelovu editaci,
aniž mu ukázal diff. Opraveno na dva kroky (`--check --new-text-file` → po `ok` totéž bez
`--check`) + věta, že temp soubor musí zůstat (uživatelova verze se do `patch` neukládá).
- Zkrácení: **1 981 → 1 747 slov, 236 → 220 řádků** (12 %). Škrtalo se výhradně
odůvodnění návrhu, které je v `README.md`, a trojité definice pojmů.
- Nový test `test_check_previews_the_users_own_rewrite` — kombinaci `--check`
+ `--new-text-file`, kterou teď SKILL.md předepisuje, nekryl žádný test. 168 testů zeleně.
- Verifikace: skriptovaný checklist 41 instrukcí (gates, řadicí klíče, stale, display IDs,
pásma, audit) proti novému textu → 41/41 přítomno. Nasazeno rsyncem, `nanobot:nanobot`,
smoke test `reflect_apply.py --id f7575 --check` na serveru vytiskl diff, exit 0, nic
nezměnil.
**Co zbývá:** slíbil jsem v review i v plánu „30 %", ale itemizovaný seznam škrtů sečteno
dával jen ~17 % a tabulka v plánu ~1 650 slov — headline číslo bylo aritmetika, kterou jsem
si neověřil. Reálný výsledek je 12 %. Hlubší škrt (30 %) by znamenal sáhnout na věci
označené jako load-bearing: seznam polí v `Data`, šablonu předložení nálezu a `Decision
history`. Čeká na rozhodnutí autora.
**Zamítnuto během review:**
- doplnit ke gate 1 vysvětlení, proč je u `uv` plná cesta — agent běží ve workspace, ne přes
SSH; příkaz jen relayuje uživateli a důvod znát nemusí (viz záznam 14:15).
- opravit mapování „exit 2 = patch neplatí" ve step 2 — nález ve step 2 je vždy `open`
a s patchem, takže ostatní důvody odmítnutí jsou nedosažitelné; oprava by přidala slova.
- doplnit do `README.md` větu, proč je `occurrences` až poslední řadicí klíč — nikdo ten
detail nepotřebuje, plná úvaha je v commitu aef90f3.
**Jak vrátit zpět:** `git revert 917afb9` + rsync `skills/reflect/` na server.

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.