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

View File

@@ -260,12 +260,21 @@ Modely se přidávají jako položky do `model_presets` v `~/.nanobot/config.jso
4. **V chatu** (Telegram/WebUI) přepneš příkazem `/model <preset-name>`.
**Konvence pojmenování presetů:** krátký alias podle modelu — `kimi`, `kimi27`, `kimi3`, `glm`, `glm52`, `sonnet`, `haiku`, `gemini-flash`. (Dřív tu stálo `<model>-<provider>` jako `kimi-k2.6-openrouter`; reálný stav na serveru je od nějaké doby krátká forma, ověřeno 2026-07-27.)
**Konvence pojmenování presetů:** krátký alias podle modelu. Aktuální sada (ověřeno 2026-09-16): `glm`, `flash`, `glm52`, `glm51`, `kimi`, `kimi26`, `kimi3`, `sonnet`, `haiku`, `gemini-flash`, `gemini-flash-lite`. **Nepojmenovaný alias = aktuální generace** (`glm` = glm-5.3, `kimi` = kimi-k2.7-code), číslovaný = zamrzlá starší verze. (Dřív tu stálo `<model>-<provider>` jako `kimi-k2.6-openrouter`, pak `glm53`/`kimi27`; 2026-09-16 přejmenováno na současnou formu.)
**Parametry presetu:** `maxTokens` 16384 a `temperature` 0.1 napříč všemi presety. `contextWindowTokens` se drží na **~97 % reálného okna modelu** (rezerva na výstup), reálné okno se čte z `curl http://nvidia.hell:11434/api/show -d '{"model":"<id>"}'` → `model_info["<family>.context_length"]`. `reasoningEffort: null` = zachovat default providera (`schema.py:141`), explicitní hodnota jen kde ji chceme vynutit (`glm52: high`).
**Ollama gotcha:** `providers.ollama.apiBase` musí končit `/v1` (`http://nvidia.hell:11434/v1`) — viz [[Ollama provider potřebuje `/v1` suffix v `apiBase`]].
**Gotcha — přejmenování presetu validace nechytí.** `Config._validate_model_preset` (nanobot `config/schema.py`, model_validator mode="after") ověřuje při startu **jen** `agents.defaults.modelPreset` a `fallbackModels`. Preset předaný ad-hoc do `Nanobot.from_config(model_preset=...)` neprojde žádnou statickou kontrolou a spadne až za běhu v `Config.resolve_preset()` — u nočních cron jobů tedy ve 2:00 do logu a Telegramu, ne při editaci configu. **Po každém přejmenování presetu projdi tahle místa:**
- `skills/reflect/scripts/reflect_auto.py` → `MODEL_PRESET`
- `skills/compact-memory/scripts/compact_memory_auto.py` → `MODEL_PRESET`
- serverový `~/.nanobot/workspace/knowledge/models.md` — tabulka presetů, podle které si agent vybírá `/model`; neplatný alias tam je tichá chyba (agent zkusí neexistující preset)
- `skills/detach/` řešit nemusíš — preset resolvuje dynamicky přes `load_preset_names()`/`resolve_preset()`
Jednorázový check: `ssh nanobot@nanobot.hell 'grep -rn -e <starý-alias> ~/.nanobot/workspace/skills/ ~/.nanobot/workspace/knowledge/ ~/.nanobot/config.json'` (pozor na `__pycache__` — zastaralý `.pyc` matchne, ale nic neovlivní). Automatický validátor uživatel 2026-09-16 explicitně zamítl: Telegram alert z nočního běhu je jako detekce dostatečný. Plný záznam: history 2026-09-16.
## Ollama Cloud: některé modely jsou „extra usage only" (kimi-k3)
`kimi-k3:cloud` je v `api/tags` vidět a `/model kimi3` v nanobotu se přepne bez chyby, ale **každé volání skončí HTTP 402**:
@@ -792,7 +801,7 @@ Nanobot má **hardcoded default `context_window_tokens = 65_536`** pro `ModelPre
Nastaveno 2026-06-02 per-preset na reálné limity modelů (kimi-k2.6 / qwen3.5 / nemotron-3-super 262144, minimax-m2.7 204800, glm-5.1 196608, deepseek-v4-flash 1048576) + `maxTokens` 16384. **Bez restartu** — `modelPresets` se hot-reloadují (viz sekce „Kdy je a není potřeba restart"). U `:cloud` modelů hostí kontext Ollama cloud, takže `contextWindowTokens` reálně rozšíří budget — není to lokální `num_ctx` žeroucí RAM. Plný záznam: history 2026-06-02.
**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.
**Gotcha — `agents.defaults.contextWindowTokens` je zavádějící číslo.** V serverovém `config.json` je `65536`, ale to platí jen když preset vlastní hodnotu nemá. Reálné okno aktuálního defaultu (**`glm`: 976 000**, `flash`/`glm52` 976 000, `kimi3` 1 020 000, `sonnet`/`gemini-flash` 256 000) je ~15× větší, než `defaults` napovídá. **Než z 65k něco odvodíš, přečti `model_presets`, ne `agents.defaults`** — na tomhle jsem 2026-09-02 postavil celý (zamítnutý) rozpočet velikosti pro `/project`. Limit, který v praxi kouše, je `maxToolResultChars: 16000`, ne okno.
**Důsledky (trade-off, ne čistá výhra):**
@@ -1137,7 +1146,7 @@ Naměřeno na reálném destilátu (90 kB promptu → 75 135 tokenů podle tikto
## `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.
`agents.defaults.contextWindowTokens` je 65536, ale preset `glm` má 976000 a **vyhrává**`agent/loop.py:476` (`context_window_tokens = extra.pop(...) or resolved.context_window_tokens`). Efektivní input budget = `contextWindowTokens - maxTokens - 1024` (`SNIP_SAFETY_BUFFER`, `agent/context_governance.py:105`), tedy ~958k tokenů pro `glm`.
## Velká zpráva projde, `snip_history` krátí jen historii
@@ -1302,3 +1311,54 @@ per model. Ověřeno 2026-09-15.
→ Atribuce spotřeby na session jde jen **časovou korelací** (timestampy v `sessions/*.jsonl`
a `Processing message from …` v journalu) proti řadě vzorků z `db/ollama_usage.sqlite`.
Ověřeno 2026-09-15, zdroj: [[plans/ollama-usage-poller.md]] sekce Revize.
---
## Ollama session okno: 5 h kotvených prvním requestem, ne pevná mřížka
Okno **nezačíná na pevné mřížce** — začíná prvním requestem po vypršení
předchozího (jako 5h sessions v Claude Code). Naměřeno 2026-09-15: usage
zůstala na `0.077`/21 req přes 05:00 UTC (pevná mřížka by ji tam vynulovala
a poller by to zapsal) a resetovala se až s requestem v 06:00, po 93minutové
pauze. Rekonstrukce aktivity z `sessions/*.jsonl` dává souvislý řetězec
00:0005:00, pak 06:0011:00 UTC.
Délka 5 h je z primárního zdroje
[transparent-pricing](https://ollama.com/blog/transparent-pricing) („no 5-hour
or weekly limits" je to, co **nové** plány zrušily; tenhle klíč je pořád má).
Že jde o okno a ne rolling counter je taky měřené — pokles `0.077`/21 → `0.0`/`{}`
naráz, rolling by se rozpadal postupně.
**Praktický důsledek:** konec okna = poslední pozorovaný přechod v `samples`
+ 5 h. Za konec se nesmí extrapolovat — další okno začne až dalším requestem,
takže čas neexistuje, dokud request nepřijde. Detail: history 2026-09-15 10:15.
**Pozor na past:** crony nanobota běží na celé hodiny (Dream po 2 h atd.), takže
první request po pauze často padne na celou hodinu. Hranice pak *vypadají* jako
pevná mřížka, i když nejsou.
**Zamítnuto:** kalibrace fáze průnikem kandidátů modulo 300 min. Stála na
předpokladu pevné mřížky a navíc potřebovala víc pozorování, než začala hlásit
cokoli. Nahrazeno výše. Vedlejší poznatek, který z ní zbyl: **5 h nedělí 24 h**,
takže „mřížka 00/05/10/15/20 UTC" nemůže být stálá tak jako tak.
## `limits.session.usage` == procento na dashboardu; countdown na webu ne
API `2.3 %` sedělo přesně s tím, co uživatel viděl na webu. **Countdown na webu
je ale ke kalibraci nepoužitelný** — zaokrouhluje neznámo jak: v 06:58 UTC
ukazoval „4 hodiny" a v 07:05 „3 hodiny". Proto skill tiskne i konkrétní čas,
ne jen „resets in X".
## Zamítnuto: heartbeat v usage polleru
Nápad zapisovat vzorek i beze změny (aby mezera v `samples` znamenala výpadek)
je **zbytečný** — interval `(poslední před, první po]` kolem resetu je pravdivý
bez ohledu na to, jestli poller běžel; výpadek ho jen rozšíří. Uživatel ho
zamítl a měl pravdu: DB zůstává čistě změnová.
## `uv` není v PATH při neinteraktivním SSH
`ssh nanobot@nanobot.hell 'uv run …'` selže na `uv: command not found` — je
v `~/.local/bin`, který se nenačte. Crontab to řeší vlastním řádkem
`PATH=/home/nanobot/.local/bin:/usr/bin:/bin`. Při ručním spuštění přes SSH
předřadit `export PATH=/home/nanobot/.local/bin:$PATH`.

View File

@@ -31,3 +31,60 @@ Po každém commitu, který mění `knowledge.md`, `history.md` nebo `memory.md`
**Why:** Uživatel 2026-06-02 chtěl agentovi zpřístupnit develop kontext a zvolil průběžnou synchronizaci (ne jednorázovou kopii) — jinak agent časem uvidí zastaralý stav.
**How to apply:** `rsync -av <soubor> nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/develop/`. `README.md` v `develop/` je statický popis, ten se nesynchronizuje. Owner zůstává `nanobot:nanobot` (jdeme jako `nanobot`).
## feedback: než navrhneš řešení, nejdřív se podívej na reálný stav souborů na serveru
Když uživatel navrhuje změnu/přidání do serverové konfigurace (skill, `SOUL.md`/`AGENTS.md`/ostatní workspace soubory, config), **nejdřív si stáhni a přečti aktuální serverovou verzi** a ověř, zda navrhovaná věc už neexistuje nebo není vyřešená jinak — teprve pak navrhuj postup.
**Why:** Uživatel 2026-06-07 — na otázku „má smysl zapsat nanobotovi reasoning anglicky?" jsem rovnou navrhl formulaci a celý deployment, ale pravidlo už v `SOUL.md` dávno bylo (napsal si ho Dream procesor sám 2026-05-27). Celý návrh byl zbytečný. Uživatel to označil za podstatnější poznatek než samotnou odpověď. Server se navíc mění autonomně (Dream), takže předpoklady z paměti/repa můžou být zastaralé.
**How to apply:** U čehokoli, co se týká serverového stavu, je první krok `rsync`/`ssh cat` reálného souboru + kontrola, jestli problém už není vyřešený. Návrh řešení až po ověření. Platí i pro „malé" změny, které vypadají triviálně.
## feedback: u bugu nejdřív najdi a dolož PŘÍČINU, neiteruj workaroundy
Než navrhnu jakýkoli fix chování (zvlášť rendering/UI bug), musím **nejdřív najít a doložit kořenovou příčinu** — přečíst reálný **nasazený** kód, který chování produkuje (ne upstream/podobnou verzi), a získat **přímý důkaz** (např. session log s tím, co model skutečně vrátil). Teprve s prokázanou příčinou navrhovat řešení.
**Why:** 2026-06-14 (`/note` + URL ve WebUI, viz [[plans/note-wrong-urls.md]]) jsem několik kol nasazoval kosmetické obezličky (linkify → backtick → odrážky → tučné číslo), všechny selhaly, a teprve pak našel příčinu: custom fork WebUI má `li` handler, co přebalí každou položku seznamu s odkazem na kartu. Kdybych nejdřív přečetl nasazený renderer a session log (důkaz, že model echovuje verbatim → chyba je v rendereru, ne v modelu/formátu), ušetřil bych celá kola deploy-test a autorovo zklamání. Nabízená „řešení" pak byly workaroundy, ne systémová oprava — autor je všechny zamítl jako „nesystémové".
**How to apply:** U bug reportu: (1) lokalizuj a přečti reálný nasazený kód zodpovědný za chování; (2) seženi přímý důkaz, kde přesně se to láme (logy, session transcript, raw výstup) a vyluč nesprávné hypotézy (model vs renderer apod.); (3) až pak navrhuj fix — a měř ho proti příčině: pokud neopravuje příčinu, řekni to nahlas a označ za workaround. Doplňuje [[memory.md]] „než navrhneš řešení, nejdřív se podívej na reálný stav souborů na serveru".
## feedback: skill nemá opakovat ani vysvětlovat to, co už je v system promptu
Do `SKILL.md` (nanobotího skillu) nepatří konvence a fakta o prostředí, která už žijí
v system promptu — `AGENTS.md` (nástroje, `uv`, temp soubory, exec guard, git) a `SOUL.md`
(osobnost, styl výstupu, jazyk reasoningu). Skill popisuje **svůj vlastní postup**, ne to,
jak se v tomhle prostředí obecně pracuje. Než něco takového do skillu napíšu, ověřit
`grep` v `AGENTS.md`/`SOUL.md`, jestli to tam už není.
**Why:** 2026-09-02 jsem při zkracování skillu `reflect` do STOP gate 1 *přidal* půlvětu
vysvětlující, proč se `uv` volá plnou cestou („`uv` není v `PATH` v neinteraktivním SSH;
uvnitř tahu stačí `uv run`"). Uživatel se zeptal, proč to tam vůbec je — `AGENTS.md`
celou sekci `## python — use uv`. Byla to dvojí chyba: environmentální meta-znalost ve
skillu, a druhá polovina věty navíc doslova opakovala, co `AGENTS.md` agentovi říká.
Zvlášť trapné v commitu, jehož cílem bylo skill **zkrátit**. Viz `history.md` 2026-09-02
14:15.
**How to apply:** Fakt o prostředí → `AGENTS.md`/`SOUL.md`. Vysvětlení „proč je ten příkaz
takhle" pro člověka → `README.md` skillu (nenačítá se do kontextu, takže nestojí tokeny).
Do `SKILL.md` jen to, co agent potřebuje k provedení **tohoto** postupu. A pozor na
asymetrii: přidat do system promptu se vyplatí jen tehdy, když to agent reálně potřebuje —
`PATH` gotcha se do `AGENTS.md` nakonec taky nepřidala, protože agentovi bare `uv run`
funguje a týkala se jen člověka v SSH. Souvisí s [[memory.md]] „než navrhneš řešení,
nejdřív se podívej na reálný stav souborů na serveru".
## feedback: YAML seznamy — víc položek nebo dlouhé stringy jdou do block stylu
Seznam s jedinou krátkou položkou zůstává flow (`include: ["*.md"]`, `paths: ["**"]`,
`exclude: []`). Jakmile má **víc položek** nebo jsou položky **dlouhé stringy** (typicky
cesty a globy), píše se **block stylem, každá položka na vlastním řádku**.
**Why:** 2026-09-09 jsem v plánu `final-wiki-hybrid-rag` napsal `exclude` jako flow seznam
zalomený přes dva řádky (`["**/node_modules/**", "**/vendor/**",` / `"**/.venv/**", …]`).
Uživatel to vrátil: zalomený flow seznam je nejhorší z obou světů — nevejde se na řádek,
nejde u položky mít komentář a diff jedné změněné položky přepíše celý blok.
**How to apply:** Platí pro jakýkoli YAML, který píšu nebo který generuje kód (configy
skillů, frontmatter, CI). Rozhoduj podle obsahu, ne podle délky výsledku: dva dlouhé globy
jdou do block stylu, i kdyby se na jeden řádek vešly. Block styl navíc umožní komentář
u konkrétní položky, což u whitelistů a excludů nese hodnotu. Souvisí s pravidlem
v `CLAUDE.md`, že config je YAML právě kvůli komentářům.

View File

@@ -7,44 +7,44 @@ trvalá změna defaultu = `agents.defaults.modelPreset` v `~/.nanobot/config.jso
## Nakonfigurované presety (`/model`)
Zdroj: `~/.nanobot/config.json``model_presets`. Všechny mají `maxTokens = 16384`,
`temperature = 0.1`, `provider = ollama`.
Zdroj: `~/.nanobot/config.json``model_presets`. Všechny mají `maxTokens = 16384`
a `temperature = 0.1`. Default je `glm`, fallback `kimi`.
| Preset | Model id | Kontext | reasoningEffort |
|---|---|---|---|
| `glm` *(default)* | `glm-5.3-flash:cloud` | 976 000 | high |
| `glm53` | `glm-5.3:cloud` | 976 000 | high |
| `glm52` *(legacy)* | `glm-5.2:cloud` | 976 000 | high |
| `glmi51` *(legacy)* | `glm-5.1:cloud` | 196 608 | — |
| `kimi` | `kimi-k2.6:cloud` | 262 144 | — |
| `kimi27` | `kimi-k2.7-code:cloud` | 262 144 | — |
| `kimi3` | `kimi-k3:cloud` | 1 020 000 | — |
| Preset | Provider | Model id | Kontext | reasoningEffort |
|---|---|---|---|---|
| `glm` *(default)* | ollama | `glm-5.3:cloud` | 976 000 | high |
| `flash` | ollama | `glm-5.3-flash:cloud` | 976 000 | high |
| `glm52` *(legacy)* | ollama | `glm-5.2:cloud` | 976 000 | high |
| `glm51` *(legacy)* | ollama | `glm-5.1:cloud` | 196 608 | — |
| `kimi` | ollama | `kimi-k2.7-code:cloud` | 262 144 | — |
| `kimi26` | ollama | `kimi-k2.6:cloud` | 262 144 | — |
| `kimi3` | ollama | `kimi-k3:cloud` | 1 020 000 | — |
| `sonnet` | openrouter | `anthropic/claude-sonnet-4.6` | 256 000 | — |
| `haiku` | openrouter | `anthropic/claude-haiku-4.5` | 200 000 | — |
| `gemini-flash` | openrouter | `google/gemini-3.5-flash` | 256 000 | — |
| `gemini-flash-lite` | openrouter | `google/gemini-3.1-flash-lite` | 256 000 | — |
`glmi51` a `glm52` jsou legacní — jen pro regression srovnání, k práci nepoužívat.
`glm51` a `glm52` jsou legacní — jen pro regression srovnání, k práci nepoužívat.
`kimi3` nepoužívat (viz níž), zvaž odstranění presetu z configu.
---
## Strategie modelů (potvrzeno uživatelem, 2026-02)
## Strategie modelů (potvrzeno uživatelem, 2026-09)
| Role | Model |
|---|---|
| **Daily driver** | `glm` — glm-5.3-flash |
| **Těžké úlohy** | `glm53` — glm-5.3 |
| **Kódování** | `kimi27` — kimi-k2.7-code (fallback: `kimi`) |
| **Běžné agentní práce na Kimi** | `kimi` — kimi-k2.6 |
| **Daily driver + těžké úlohy** | `glm` — glm-5.3 |
| **Kódování** | `kimi` — kimi-k2.7-code |
| **Běžné agentní práce na Kimi** | `kimi26` — kimi-k2.6 |
| **Levnější/rychlejší + multimodální vstup** | `flash` — glm-5.3-flash |
| **Nepoužívat** | `kimi3` — extra high usage, zasekávání v agentním modu (2026-02) |
### Kdy přepnout na glm53 („těžká úloha")
### Kdy sáhnout po `flash`
- Multi-step research / deep-research skill (30+ iterací, cross-checking, dlouhé syntézy).
- Multi-file refactor (5+ souborů, velký kontext).
- Dlouhý debug / root-cause analýza přes víc systémů.
- Wiki-compile s velkými vstupy (drain tisíců řádků).
- Prompt injection / security analýza.
Pravidlo: nejdřív `glm` (flash); když task „cuchne" (model ztrácí nit, opakuje tool
call, potřebuje přes 100 iterací), restartuj na `glm53`.
- **Multimodální vstup** — obrázek, screenshot, sken, video. `glm` (glm-5.3) je
**text-only**, flash je jediný GLM preset, co obrázky přijme (viz níž).
- Levné rutinní tahy, kde nejde o maximální kvalitu a záleží na rychlosti
(TTFT ~0,4 s vs ~0,7 s).
### GLM-5.3 řada — srovnání (deep research, 2026-06)