This commit is contained in:
lachtan
2026-09-16 08:56:28 +02:00
parent b0827177d2
commit 5407931bdc
4 changed files with 550 additions and 29 deletions

View File

@@ -1,5 +1,134 @@
# History
## 2026-09-11 — AGENTS.md: pravidlo o exec guardu globálně + srovnání skillu `note`
**Cíl:** Navázat na opravu skillu `project` (záznam níže) a zapsat pravidlo o exec
guardu na místo, kde pomůže všem skillům, ne jen tomu, kde se zrovna projevilo.
**Co jsem zkusil / co jsem našel:**
Nejdřív jsem zjišťoval, který soubor nanobot vůbec načítá do system promptu.
`build_system_prompt()` (`agent/context.py:70121`) skládá prompt z
`BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md"]`. Z nich je **`AGENTS.md` jediný
user-ownovaný** — `SOUL.md`, `USER.md` i `memory/MEMORY.md` přepisuje Dream, bundled
`templates/` přepíše upgrade balíčku.
Při čtení `AGENTS.md` vyšlo najevo, že **tentýž bug už tam byl zdokumentovaný dvakrát
izolovaně**: sekce `## Git commit timestamps` říká, že `date '+%Y-%m-%d %H:%M:%S'` guard
blokuje (ověřeno — token `H:%M:%S`, `H` po `%`), a sekce `## exec Tool` řešila chybějící
workspace path. Ani jednou se nepojmenovala společná příčina. Třetí výskyt byl živý:
`note_capture.py --text "Cíl: koupit mléko"` → blok (token `l:`), přičemž
`note/SKILL.md` explicitně nařizuje „Pass the input **as-is**". Poznámka uživatele
začínající `Cíl:` tedy tiše selhávala — a capture bere vstup doslova, takže to nebyl
okrajový případ, ale běžná česká formulace.
**Co fungovalo a proč:**
- **`workspace/AGENTS.md`** — nejdřív dotažena autoritativní serverová verze do repa
samostatným commitem (lokální kopie byla zastaralá a server je zdroj pravdy), teprve
pak přidáno pravidlo do sekce `## exec Tool`: text nikdy do command stringu, vždy
`write_file` do `tmp/` + předání cesty. Sekce `## Git commit timestamps` ponechána —
jmenuje konkrétní skript a je jen instancí obecného pravidla.
- **`skills/note`** — `--text` nahrazen za `--file <path>` (fail fast na chybějící
soubor), obě volací místa v SKILL.md převedena na dvoukrok, přidána sekce *Never pass
the text on the command line*. Testy 19 → 22.
Ověřeno na serveru proti **nainstalovanému** guardu:
```text
PROJDE NOVY --file -> []
BLOKOVAN STARY --text -> ['l:', 'r:']
```
End-to-end: `write_file` textu `Cíl: koupit mléko. Závěr: zítra.` + `--file` → soubor
v `notes/inbox/` s korektním frontmatterem, exit 0. Inbox pak uklizen, **`log/note.log`
ne** — je to append-only audit log a mazat ho by porušilo přesně to pravidlo, které
tahle práce kodifikuje. Zůstal tam jeden testovací CAPTURE řádek z 15:09.
Ověřeno i to, že se `AGENTS.md` do promptu opravdu dostává (liší se od
`templates/AGENTS.md`; při shodě by ho `context.py:179182` tiše vynechalo).
**Co zbývá:**
- `bookmark` (český článek v heredocu) a `remind` (`edit --text`) mají pořád stejnou
expozici — vědomě mimo scope, vedeno v `knowledge.md`.
- `AGENTS.md` se bere z `project_root` aktuálního tahu (`context.py:163`), takže
v session scoped do `tmp/<x>` se root verze nenačte. Pro chat workflow to nevadí;
univerzální varianta by byl skill s `always: true`.
- Regex v nanobotu zůstává rozbitý; tohle je workaround, ne fix.
**Jak to vrátit zpět:** `git revert` příslušných commitů + rsync `workspace/AGENTS.md`
a `skills/note/` na server. Nic se nemazalo kromě vlastních testovacích artefaktů.
## 2026-09-11 — project: zápis do memory.md padal na exec guardu, převeden na --file
**Cíl:** Agent na serveru nedokázal zapsat záznam do `projects/life/memory.md`
tři pokusy po sobě spadly na `Command blocked by safety guard (path outside
working dir)`. Zjistit proč a opravit skill, aby se to neopakovalo.
**Co jsem zkusil / co jsem našel:**
Stažena session `002f2196` z `~/.nanobot/webui/`. Pořadí selhání: quoted heredoc
`printf | pipe``--text`, všechny se stejnou chybou; uspěl až `write_file` do
`tmp/` + `log life < tmp/soubor`. Agent si to ve vlastním shrnutí vyložil jako
„nedeterministický blok na datu a tečkách" — **to je mylné**.
Skutečná příčina, ověřená spuštěním nainstalovaného guardu
(`/home/nanobot/.local/share/uv/tools/nanobot-ai/…/nanobot/agent/tools/shell.py`)
na reálných příkazech: `ExecTool._extract_absolute_paths()` hledá windowsové cesty
regexem s **ASCII-only negativním lookbehindem** `(?<![A-Za-z])[A-Za-z]:…`. Znak
s diakritikou před ASCII písmenem lookbehind neutne, takže `Cíl:` → token `l:`,
`Závěr:`/`směr:``r:`. `Path("r:").resolve()` se rozvine vůči cwd démona
(`/home/nanobot`, ne vůči workdiru příkazu) → `/home/nanobot/r:` → mimo workspace
→ blok. Guard navíc jede nad **raw command stringem bez shell parseru**, takže
nerozliší argument od obsahu heredocu — proto padly všechny tři varianty stejně.
Blokovaný text obsahoval `Diagnóza/směr:`; `artifacts/fyzio-cviky-kycele.md` ani
lomítko samo o sobě vinné nebyly.
Kontrolní vzorek proti nainstalovanému guardu (`--file` vs. staré varianty):
```text
PROJDE NOVY --file -> []
BLOKOVAN STARY heredoc -> ['l:', 'r:', 'r:']
BLOKOVAN STARY --text -> ['l:', 'r:', 'r:']
BLOKOVAN STARY printf|pipe -> ['l:', 'r:', 'r:']
```
**Co fungovalo a proč:**
Oprava není „jiný způsob citace", ale **text se nikdy nesmí dostat do command
stringu**. Konkrétně:
- `project_cli.py`: `--text` odstraněn, přidán `--file <path>` (fail fast na
neexistující soubor). Stdin zůstal jako fallback. Flag místo shell redirectu
proto, že celý failure mode byl model improvizující shell syntax.
- `SKILL.md`: sekce „Writing to memory.md" přepsána na dvoukrok `write_file` do
`tmp/` + `log <slug> --file …`, s vysvětlením **proč**, ať to nikdo
neoptimalizuje zpátky na heredoc.
- `SKILL.md` Rules: zákaz testovacích zápisů do ostrého projektu — agent si při
diagnostice zanesl do append-only `memory.md` dvě test položky a pak je ručně
odstranil, čímž porušil „never rewrite history". Od toho je `new <scratch-slug>`.
- Testy: 15 → 20. Sekce `log` převedena na fixture `entry_file`, takže testuje
reálnou cestu přes soubor. Přibyl regresní test s `Cíl:`/`Závěr:`, test na
chybějící soubor a pojistka, že `--text` v argparse skončí `SystemExit 2`.
Před nasazením ověřena shoda serveru s repem (`rsync` + `diff -r` → lišil se jen
`__pycache__`). Nasazeno rsyncem, vlastník `nanobot:nanobot` ověřen, restart
služby není potřeba. End-to-end test proti scratch projektu `guard-test` (ne proti
`life`) s textem `Cíl: … Diagnóza/směr: … Závěr: …` prošel, exit 0; scratch
projekt i `tmp/guard-test.md` pak smazány.
**Co zbývá:**
Guard v nanobotu je pořád rozbitý a **stejnou expozici mají další skilly**
`note` (`--text "<raw input>"` se syrovým českým vstupem uživatele, nejrizikovější),
`bookmark` (český článek v heredocu) a `remind` (`edit --text`). Vědomě mimo scope
této opravy, zapsáno do `knowledge.md` jako známý dluh. Plošný fix by znamenal
unicode-aware lookbehind v upstreamu (resp. ve forku `nanobot-custom-commands`).
**Jak to vrátit zpět:** `git revert` commitu se skillem + rsync `skills/project/`
na server. Žádná data se nemazala kromě scratch projektu `guard-test`.
## 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
@@ -3455,3 +3584,278 @@ návrh do `decisions.md`) se nemění.
**Jak vrátit zpět:** `git revert 3adc2ef`, pak na serveru rsync skillu a
`wiki_sync.py --full` (titulky se vrátí na NULL). Index se tím nerozbije — `title` je
jen sloupec v katalogu, retrieval na něm nestojí.
---
## 2026-09-15 06:20 — Ollama usage poller: revize serverového plánu + nasazení sběru
**Cíl:** Uživatel chtěl dotáhnout serverový plán `plans/ollama-usage-poller.md`
(napsal si ho nanobot sám 2026-09-14) do repa a probrat, jak ho realizovat a kde
má slabiny. Záměr plánu: minutový cron → SQLite, aby šlo zpětně zjistit, kolik
která session stála.
**Co jsem zkusil:** Nejdřív ověření předpokladů plánu proti reálnému serveru
(ne proti tomu, co plán tvrdí). Nálezy, kvůli kterým se návrh měnil:
- **`memory/history.jsonl` nemá tokeny.** Plán na něm stavěl budoucí atribuci
(„per-request timestamps, token counts, session ids"). Reálně obsahuje jen
`{cursor, timestamp, content}` (+ `session_key` u 59 ze 430 řádků) — je to
destilát paměti z Dreamu. `sessions/*.jsonl` má `role/content/timestamp/
tool_calls/reasoning_content/latency_ms`, **taky bez tokenů a bez modelu**.
Jediný zdroj tokenů je `LLM usage:` v journalu, což je DEBUG a `-v` už v
`ExecStart` **není** (poslední takový řádek je z 2026-05-27) — `knowledge.md`
tvrdila opak, opraveno.
- **`limits.*.usage` má rozlišení 0,1 %** (API vrací 3 desetinná místa).
Plánovaný report `Δsession %, Δweekly %` per dvojice vzorků sliboval přesnost,
kterou data nemají → osa reportu přepnuta na `request_count` per model.
- **`models` je pole `[{name, request_count}]`**, ne slovník jak tvrdil komentář
ve schématu → kanonizace na setříděný dict, jinak by se zapisovalo každou minutu.
- **Umístění `workspace/scripts/`** odporovalo vzoru — všech 6 existujících cron
entry míří do `skills/<name>/scripts/` → přesunuto do `skills/usage/scripts/`,
čímž odpadl i `sys.path` hack kolem reuse `load_api_key`.
- **Write-on-change nerozliší klid od výpadku** → doplněna tabulka
`meta(last_ts, last_status)`, přepisovaná při každém běhu včetně chybového.
- Ukládá se i `session.models` (plán si nechával jen `weekly`).
**Co fungovalo a proč:** Skill `usage` dotažen do repa (dosud tam vůbec nebyl),
přidány `ollama_usage_poll.py` + `ollama_usage_report.py` (stdlib only, EN),
sekce „Continuous sampling" do `SKILL.md`, rsync na server (`nanobot:nanobot`),
cron entry `* * * * *`. Ověřeno:
1. Dva běhy po sobě → `samples` má 1 řádek, `meta.last_ts` se posunulo oběma
(04:19:46 → 04:19:53). Write-on-change funguje.
2. `OLLAMA_API_KEY=bogus` → exit 0, `meta.last_status = 'http_401'`,
žádný nový řádek. Cron se tím nezanáší.
3. Logika reportu ověřena na syntetické DB ve scratchpadu: hranice session okna
(pokles `session_usage` i součtu `session_models`), mezera v datech,
per-model delty, souhrn.
4. Cron potvrzen (`meta.last_ts` se posouvá bez ručního spuštění,
`log/ollama_usage_cron.log` prázdný).
**Co zbývá:** Atribuce na sessions — přesunuto do `todo.md`, až bude pár dní dat.
Vedlejší přínos, který plán nepojmenoval: minutová řada ověří/vyvrátí domněnku
ve `skills/usage/SKILL.md`, že session okno je kalendářní hodina
(`until_next_full_hour` je odhad, ne údaj z API).
**Jak vrátit zpět:** `crontab -e` a smazat entry `ollama-usage`;
`rm ~/.nanobot/workspace/db/ollama_usage.sqlite`;
`rm ~/.nanobot/workspace/skills/usage/scripts/ollama_usage_{poll,report}.py`
a rsync `SKILL.md` z gitu před commitem. Lokálně `git revert`.
---
## 2026-09-15 10:05 — Session okno: fixní 5h blok, fáze kalibrovaná z DB
**Cíl:** Skill `/usage` hlásil „Session: … resets in 2 minutes", zatímco web
ukazoval 4 hodiny. Zjistit proč a dát tomu odhad, který se dá věřit.
**Co jsem zkusil:**
1. Porovnání serveru s repem — server běžel se starou verzí z 14. 9.
(`until_next_full_hour()`). **Oprava z předchozí session se nikdy nenasadila**
a ležela necommitnutá v repu. To je celé vysvětlení „2 minut" (běželo 06:58 UTC).
2. Data z `db/ollama_usage.sqlite`: session spadla z `0.077`/21 req na `0.0`/`{}`
mezi vzorky 04:28:01 a 06:00:01 → **fixní blok, ne rolling okno** (rolling by
se rozpadal postupně).
3. Kalibrace proti webu (uživatel hlásil, co vidí): ~06:58 „4 hodiny",
~07:05 „3 hodiny", session **2,3 %** — procento přesně sedí s API, takže
dashboard a `/api/usage` čtou totéž.
4. Délka okna z primárního zdroje — [transparent-pricing](https://ollama.com/blog/transparent-pricing):
nové plány zrušily „5-hour or weekly limits", tenhle klíč je má → **5 h**.
5. Původní návrh přidával do polleru heartbeat, aby šlo mezeru v `samples`
odlišit od výpadku. **Uživatel ho zamítl** — DB má zůstat čistě změnová.
Ukázalo se, že je zbytečný (viz níže).
**Co fungovalo a proč:** Mřížka je periodická, takže každý pozorovaný reset
omezuje **tutéž fázi**. Jedno pozorování ohraničí hranici intervalem
`(poslední vzorek před, první vzorek po]`; průnik těch intervalů **modulo 300
minut** fázi dopočítá. Dvě vlastnosti, proč to heartbeat nepotřebuje:
- Ohraničení platí, i když poller neběžel — **výpadek interval jen rozšíří**,
nikdy nezpůsobí špatný závěr. Není tedy co dokazovat.
- Když je délka bloku špatně, průnik se **vyprázdní** místo tiché chyby.
Dokud je kandidátů víc než `MAX_SPREAD`, reset se nevypisuje — dnešní data dávají
93 z 300, takže skill zatím hlásí holé `Session: 2.4 %`. To je správný výstup.
Nález při psaní výpisu mřížky: **5 h nedělí 24 h**, takže „mřížka 00/05/10/15/20
UTC" nemůže být stálá — buď startuje denně (s kratším posledním blokem), nebo
plave. Obě hypotézy jsou zatím konzistentní s daty a průnik je rozliší; report
proto tiskne jen **příští hranici**, ne seznam denních.
Časy ve výstupu jsou v lokální zóně serveru (`.astimezone()`, nic
nehardcodováno), ale výpočet zůstává v UTC — v lokálním čase by ho přechod na
zimní čas 25. 10. posunul o hodinu a zahodil kalibraci.
Ověřeno: 19 kontrol na syntetických datech (zúžení průniku, prázdný průnik,
bracket ≥ 300 min, drift přes den, rozestup přes DST), proti reálné DB
(93 kandidátů → `None`), formátování pod `TZ=Europe/Prague`/`UTC`/`America/New_York`,
a po nasazení přímo na serveru včetně běhu polleru. Uzavírá položku
z `todo.md` o ověření domněnky o kalendářní hodině.
**Co zbývá:** Rozhodující pozorování — příští hranice ve **12:00 nebo 13:00
CEST**. Podle toho, jak blízko ní bude poslední aktivita, se fáze zúží a skill
začne reset hlásit sám. Pak porovnat s webem.
**Jak vrátit zpět:** `git revert ff9ac3e` + rsync `skills/usage/` na server.
## 2026-09-15 10:15 — Session okno se kotví requestem, ne mřížkou (oprava modelu)
**Cíl:** Uživatel namítl dvě věci k předchozímu záznamu: (a) mlčení místo odhadu
mu nepomáhá, „lepší něco než nic"; (b) **hranice přece nemusí být pevná — nemůže
se začít počítat od prvního dotazu po pauze?**
**Co jsem zkusil:** Bod (b) je testovatelný, tak jsem šel do dat dřív, než jsem
dodělal (a). Z `sessions/*.jsonl` jsem vytáhl timestampy assistant zpráv a
poskládal bloky aktivity oddělené pauzami. Rekonstrukce (UTC):
| Okno | Start | Konec |
|---|---|---|
| W2 | 00:00 (první request po vypršení W1) | 05:00 |
| W3 | 06:00 (první request po vypršení W2) | 11:00 |
**Co fungovalo a proč:** Rozhodl **chybějící zápis v 05:00**. Kdyby existovala
pevná mřížka, usage by se v 05:00 vynulovala a poller (běží každou minutu) by
změnu zapsal. Místo toho se nezměnilo nic až do 06:00, kdy přišel první request
po 93minutové pauze. Vzorek `0.0`/`{}` v 06:00:01 tedy **není okamžik resetu, ale
okamžik startu nového okna**. Pevná mřížka by navíc vyžadovala, aby ten request
padl na hranici náhodou (~1:93).
Počty requestů to nerozhodly (23 v 00:0005:00 vs 19 v 01:0006:00 proti
hlášeným 21) — assistant zprávy nejsou 1:1 s LLM requesty.
Model přepsán na **poslední pozorovaný přechod + 5 h**. Vedlejší efekt: je to
výrazně jednodušší než průnik kandidátních fází modulo 300 min, který jsem
nasadil o hodinu dřív, a **hlásí reset okamžitě** — čímž vyřešil i bod (a).
Server teď říká `Session: 2.5 %, resets 13:00 CEST (in 2 h 47 min)`.
Za konec okna se nic neextrapoluje: po vypršení výstup řekne, že další okno
začne dalším requestem. Report dostal `Rollover gap:` řádky jako průběžný test
modelu — rozestup delší než blok potvrzuje kotvení requestem, rozestup přesně
roven bloku přes dlouhou nečinnost by ukazoval zpět na pevnou mřížku.
Ověřeno 15 kontrolami na syntetické DB (kotva, víc přechodů, chybějící DB,
vypršelé okno, okno přes přechod na zimní čas, formátování v UTC i CEST),
proti reálné DB a po nasazení na serveru.
**Poučení:** Předchozí záznam (09:xx) tvrdil „fixní blok" a stavěl na tom celou
kalibrační mašinerii. Hypotézu „start prvním requestem" jsem měl v úvahách na
začátku, ale zahodil ji bez testu — přitom šla ověřit ze `sessions/*.jsonl`,
které jsem měl celou dobu k dispozici. Past, která to držela naživu: crony
běží na celé hodiny, takže hranice *vypadaly* jako mřížka.
**Jak vrátit zpět:** `git revert 9f39d29` + rsync `skills/usage/` na server.
Předchozí (mřížkový) model je `ff9ac3e`.
## 2026-09-15 10:40 — Review a zeštíhlení skillu `usage`
**Cíl:** Projít `skills/usage/SKILL.md` — jestli splňuje náležitosti skillu a jestli
v něm nejsou zbytečnosti. Plus odpovědět, k čemu je v DB tabulka `meta`.
**Co jsem zkusil:** Přečetl SKILL.md, všechny tři skripty, `knowledge.md`, porovnal
serverovou verzi s lokální (server == HEAD, žádný drift). Review podle
`core:skill-authoring`.
**Co fungovalo a proč:**
- **Zbytečnosti.** Sekce „Reset times" byla ze ~40 řádků z větší části forenzní důkaz
— *jak* jsme model session okna naměřili. 1:1 duplikát `knowledge.md` („Ollama session
okno"), history 2026-09-15 10:15 a docstringů `window_rollovers()` / `print_window()`.
V SKILL.md se platí kontextem při každém načtení a agentovi neříká, co má dělat —
vyhozeno, důkazy zůstávají v tracking souborech a v kódu. Pravidlo „žádné modely na
Session/Weekly řádcích" bylo řečené třikrát, falzifikační odstavec o `Rollover gap:`
dvakrát. 117 → 96 řádků.
- **Nepřesnosti vůči skriptům** (všechny opraveny): `print_session()` má tři větve,
dokumentovaná byla jedna (chybělo „window expired…" a samotné procento); Weekly řádek
slíbený jako „resets in Y days", ale `fmt_delta_short()` pod 24 h tiskne hodiny;
„report ends with `Session window:`" — ve skutečnosti končí `Last poll:`; zdroj klíče
je env **pak** `workspace/.env`, ne jen `.env`.
- **Tabulka `meta` — odpověď na otázku.** Zapisuje se do ní (`record_poll()` na všech
třech cestách pollu: `ok`, `http_<kód>`, `unreachable`), ale je to **jeden řádek**
(`id = 1`), ne log. Čte ji jen `print_poll_status()` → `Last poll: <ts> (status)`.
Smysl: `samples` se plní jen při změně, takže bez `meta` nejde odlišit „nic se
nezměnilo" od „poller je mrtvý / API vrací 401". Zůstává; zapisovat vzorek při každém
pollu bylo dřív zamítnuto (knowledge „Zamítnuto: heartbeat v usage polleru"), `meta`
je právě ta zmenšená náhrada. Opraven ale **popis** v SKILL.md i v docstringu polleru
— tvrdil „records every poll, so a gap can be told apart from a failed poll", což
naznačuje historii; přepsaný řádek rozsoudí jen liveness *teď*.
- **Trigger fráze** ukotveny na „ollama" (`ollama usage/quota/credits/limits`, `how much
of the ollama plan is left`, `ollama usage report`). `"usage history"` zrušeno a obecné
`"quota"` zamítnuto uživatelem — netriggerovalo by, tahalo by skill na cizí dotazy.
- **Verifikace na serveru:** `ollama_usage.py` → `Session: 2.8 %, resets 13:00 CEST
(in 2 h 22 min)` / `Weekly: 16.2 %, resets in 5 days` + Models sekce — sedí se šablonou
řádek po řádku. Report končí `Last poll: 2026-09-15T08:37:01+00:00 (ok)`, `meta` má
jeden řádek s čerstvým timestampem. Verifikace odhalila i poslední nepřesnost: při
jediném rolloveru v historii report žádné `Rollover gap:` řádky netiskne
(`itertools.pairwise`) — formulace upravena na „from the second recorded rollover on".
Nasazeno rsyncem, md5 lokál == server, vlastník `nanobot:nanobot`, bez restartu služby.
**Co zbývá:** Nic. Commit `a7a725e`.
## 2026-09-16 07:20 — Fix po přejmenování presetů `glm53`→`glm`, `kimi27`→`kimi`
**Cíl:** Uživatel přejmenoval aliasy modelů v serverovém `config.json`. Rozbily se dvě
noční cron úlohy (`KeyError: "model_preset 'kimi27' not found"` z compact-memory,
`'glm53'` z reflectu). Vrátit je do provozu a najít i místa, kde je rozbití tiché.
**Co jsem zkusil / co jsem našel:**
Dva paralelní skeny — lokální repo a server. Aktuální stav configu: 11 presetů, default
`glm` (= `glm-5.3:cloud`), fallback `kimi` (= `kimi-k2.7-code:cloud`). Obě přejmenování
jsou 1:1 na stejný model string, takže oprava je mechanická a joby poběží na tomtéž
modelu jako dřív.
*Proč to nechytila validace:* `Config._validate_model_preset` (nanobot `config/schema.py`,
model_validator mode="after") ověřuje při startu **jen** `agents.defaults.modelPreset` a
`fallbackModels`. Obojí uživatel přejmenoval správně, takže gateway nastartovala bez
chyby. Preset předaný ad-hoc do `Nanobot.from_config(model_preset=...)` — což dělají
právě oba cron skripty — neprochází žádnou statickou kontrolou a spadne až za běhu
v `Config.resolve_preset()`.
*Tichá část (to, co „není vidět"):* serverový `~/.nanobot/workspace/knowledge/models.md`
je tabulka presetů, podle které si agent vybírá `/model`. Obsahovala neplatné aliasy —
a navíc **neodpovídala configu ani před přejmenováním**: `glm` měl model id
`glm-5.3-flash` (reálně `glm-5.3`, flash je samostatný preset `flash`), překlep `glmi51`
místo `glm51`, chyběly presety `flash`, `sonnet`, `haiku`, `gemini-flash(-lite)`, a řádek
„Kódování: `kimi27` (fallback: `kimi`)" byl po přejmenování sebereferenční nesmysl.
Uživatel rozhodl srovnat celou tabulku s realitou a sjednotit daily driver i těžké úlohy
na `glm` (glm-5.3); eskalační sekce „Kdy přepnout na glm53" tím ztratila smysl a je
nahrazena „Kdy sáhnout po `flash`" (multimodální vstup — `glm` je text-only — a levné
rutinní tahy).
*Co rozbité nebylo:* `skills/detach/` (resolvuje presety dynamicky přes
`load_preset_names()`/`resolve_preset()`), `skills/usage/`, `cron/jobs.json`
(dream/heartbeat/version-check preset nenastavují, `dream.modelOverride` je `null`),
systemd unity, `workspace/USER.md` (`Preset: glm` je dál platné), fronty
`tasks/{inbox,new,running}`. Komentáře v `reflect_auto.py:49` a `reflect_distill.py:55`
zmiňují **model** `glm-5.3:cloud`, ne preset — zůstaly.
**Co fungovalo a proč:**
1. Nejdřív `rsync` serverového `knowledge/models.md` do repa vlastním commitem (`3d17bf8`) —
lokální verze byla z 2026-06 a úplně jiný soubor, slepý přepis by serverovou verzi
(přepsanou 2026-08-29) zahodil. Pravidlo „server je zdroj pravdy" se vyplatilo.
2. `MODEL_PRESET` opraven na `"glm"` / `"kimi"`, `compact-memory/README.md` srovnán,
`knowledge/models.md` přepsán proti reálnému `modelPresets` (commit `dcb66d5`).
3. Nasazeno rsyncem celých adresářů skillů + `knowledge/models.md`, vlastník
`nanobot:nanobot`, bez restartu služby.
4. Verifikace: `Nanobot.from_config(model_preset=...)` pro `glm` i `kimi` proběhne a loguje
přepnutí na `glm-5.3:cloud` / `kimi-k2.7-code:cloud` (přesně ta cesta, co padala);
grep starých aliasů přes `skills/`, `knowledge/` a `config.json` je prázdný; reflect
`--dry-run --max-batches 1` exit 0; lokálně 219 testů zelených.
5. Grep zpočátku hlásil zbylý výskyt v `skills/reflect/scripts/__pycache__/*.pyc` —
zastaralý bytecode ze 2. 9., smazán (Python si ho vygeneruje znovu).
**Gotcha pro příště:** `~/.local/bin/uv` plnou cestou — v neinteraktivním SSH není `uv`
v `PATH` (`bash: uv: command not found`).
**Co zbývá:**
- E2E potvrzení nočního běhu (02:00 compact-memory, 03:30 reflect) — v logu nesmí být
`KeyError` a má přijít Telegram report. `compact_memory_auto.py` nemá dry-run a mutuje
`memory/MEMORY.md`, takže ruční spuštění jsem nedělal.
- Mimo rozsah, hlášeno uživateli: `src/MiniLoop/config.json` je commitnutý v gitu včetně
živého OpenRouter API klíče v plaintextu (řádek v `src/MiniLoop/.gitignore` je
zakomentovaný). Vyžaduje rotaci klíče a odstranění z historie.
- Automatický validátor presetů uživatel zamítl — Telegram alert z nočního běhu je jako
detekce dostatečný. Postup ruční kontroly zapsán do `knowledge.md`.