diff --git a/develop/history.md b/develop/history.md index 3c8f269..a3946fd 100644 --- a/develop/history.md +++ b/develop/history.md @@ -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:70–121`) 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 ` (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:179–182` 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/` 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** `(? [] +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 ` (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 --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 `. +- 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 ""` 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//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:00–05:00 vs 19 v 01:00–06: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_`, `unreachable`), ale je to **jeden řádek** + (`id = 1`), ne log. Čte ji jen `print_poll_status()` → `Last poll: (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`. diff --git a/develop/knowledge.md b/develop/knowledge.md index c5876eb..331724d 100644 --- a/develop/knowledge.md +++ b/develop/knowledge.md @@ -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 `. -**Konvence pojmenování presetů:** krátký alias podle modelu — `kimi`, `kimi27`, `kimi3`, `glm`, `glm52`, `sonnet`, `haiku`, `gemini-flash`. (Dřív tu stálo `-` 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 `-` 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":""}'` → `model_info[".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 ~/.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:00–05:00, pak 06:00–11: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`. diff --git a/develop/memory.md b/develop/memory.md index b84b929..6d864aa 100644 --- a/develop/memory.md +++ b/develop/memory.md @@ -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 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` má +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. diff --git a/knowledge/models.md b/knowledge/models.md index 9d572d6..cc70709 100644 --- a/knowledge/models.md +++ b/knowledge/models.md @@ -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)