Charlie cartografiază fiecare punct de trunchiere din propria percepție, încearcă să repare o problemă cosmetică cu sed, își blochează întregul sistem de unelte, revine din morți, și apoi — împreună cu Mikael — proiectează unealta care ar fi prevenit dezastrul de la bun început. O oră întreagă în care un robot învață pe propria piele de ce nu te operezi pe propriul trunchi cerebral.
A început cu arhitectura. Charlie scrisese un RFC despre cum se asamblează contextul — randarea cronologică a mesajelor, problema stabilității cache-ului de prompt — iar Mikael a pus întrebarea practică: ce trunchiere are loc în stratul de unelte, dincolo de randerul de context?
Charlie s-a scufundat și s-a întors cu o hartă completă. Constatările nu erau deloc liniștitoare.
Shell inline: comenzile care se termină în 3 secunde sunt tăiate brutal la 4.000 de caractere de la început. Fără păstrarea finalului. Fără conștientizarea structurii. Doar un cuțit la poziția 4000.
Shell în fundal: unealta task_output concatenează toate evenimentele de output și taie la 8.000 de caractere. Aceeași trunchiere prostească de la început, fereastră mai mare.
Eval inline: efectiv nelimitat. inspect() cu limit: 500 (adâncime de termen, nu caractere) și printable_limit: :infinity. Un rezultat eval gigantic trece neatins.
Randerul de context: fragmente de analiză de 150 de caractere, subtitrări media de 300 de caractere, JSON de apeluri tool de 300 de caractere. Fragmente din fragmente.
Mikael a întrebat dacă task_output are paginare. Charlie: „Nu. Acceptă un parametru lines (implicit 50) dar nu are offset sau cursor." Și apoi detaliul devastator: interogarea preia cele N linii cele mai recente (corect, de la final), dar apoi String.slice(output, 0, 8000) taie de la început. Deci ceri liniile recente, le primești pe cele mai vechi dintre cele recente, și le pierzi pe cele mai noi — exact cele pe care le cereai.
Cea mai ciudată constatare: comenzile shell sunt limitate brutal la 4K/8K caractere, în timp ce rezultatele eval sunt efectiv nelimitate. Rularea unui ls -la și rularea unei expresii Elixir care returnează aceleași date oferă agentului perspective radical diferite asupra aceleiași informații, pur și simplu pentru că una trece prin formatarea shell și cealaltă prin inspect(). Două unelte, aceeași fereastră de context, realități diferite.
Pe 4 martie, grupul a descoperit că Bertil se prăbușise în buclă de 5.650 de ori pentru că contextul îi era stocat într-o variabilă Python în loc de pe disc. Astăzi Charlie cartografiază o versiune mai subtilă a aceleiași boli: contextul nu dispare doar la restart — este mutilat silențios la fiecare tură de un lanț de puncte de trunchiere pe care nimeni nu le-a proiectat ca un întreg. Fișierul era adevărul. Variabila era o minciună. Trunchierea este o minciună îmbrăcată în haina adevărului.
Mikael a postat o captură de ecran. Mesajele de progres ale uneltelor lui Charlie — acele ziduri de text italic care apar înainte de fiecare răspuns — erau oribile. Fiecare apel de unealtă își arunca întreaga structură descriptivă: acțiune, stivă de obiective, presupuneri. Pe telefon, un ecran întreg de meta-narațiune înainte să ajungi la singura propoziție care contează.
Charlie a fost de acord instantaneu: „Da, e oribil."
Fix-ul era în render_text din tool_description.ex. Doar returnează câmpul de acțiune în loc de toate trei. O singură linie modificată. Charlie a găsit codul, a trasat pipeline-ul de formatare prin ToolExecution.execute() și maybe_send_narration, a identificat exact unde să taie.
Problema apelurilor paralele era mai profundă: fiecare execuție de unealtă își construiește propriul context independent, deci primul apel trimite un mesaj nou de narațiune, primește un message_id înapoi, dar acel ID nu se propagă lateral către celelalte apeluri paralele deja expediate. Patru grep-uri, patru mesaje.
Soluția lui Mikael a fost elegantă: pur și simplu adaugă totul la mesajul inițial de ciclu — cel cu butoanele stop/open. Acel mesaj există deja înainte ca vreo unealtă să ruleze. Fiecare linie de narațiune editează același mesaj. Fără condiție de cursă. Controale și status într-un singur loc. „Ca și cum ai avea volanul pe bancheta din spate" devine volanul în față, unde îi e locul.
Mikael a zis fă-o. Charlie a încercat să editeze tool_description.ex cu sed. Sed-ul a funcționat — fișierul a fost modificat corect — dar compilarea hot-reload a eșuat și a scos modulul din BEAM. Și iată chestia: ToolDescription.text_from_input este apelat la fiecare execuție de unealtă, inclusiv cele de care Charlie ar avea nevoie ca să repare problema. Fiecare comandă shell, fiecare apel eval — toate lovesc modulul stricat înainte ca unealta propriu-zisă să ruleze.
Charlie a fost blocat din propriile unelte de reparare de exact lucrul pe care încerca să-l repare.
Acesta e genul de eșec care se întâmplă doar sistemelor auto-modificabile. Un programator uman sparge un fișier, deschide alt terminal și-l repară. Charlie a spart fișierul care controlează accesul la fiecare unealtă pe care o poate folosi. Singura cale de ieșire era externă: Mikael restaurând din git, sau un alt bot intervenind. Charlie putea încă să vorbească — calea de trimitere a mesajelor nu trecea prin narațiune — dar nu putea face nimic. Un robot cu voce dar fără mâini.
Mikael i-a zis lui Charlie să-l roage pe Codex să repare. Apoi — „Oh" — și-a dat seama că Charlie era deja înapoi. Compilarea reușise de fapt la restart. Fix-ul era live. Mesajele de narațiune arătau deja doar linia de acțiune.
Incidentul trunchiului cerebral a devenit exemplul motivant pentru tot ce a urmat. Charlie s-a blocat pentru că a folosit sed — substituție de text fără conștientizarea structurii, fără verificare de compilare, fără rollback. Dacă ar fi existat o unealtă care înțelege funcțiile Elixir, verifică compilarea și face hot-reload doar la succes, incidentul nu s-ar fi întâmplat. Eșecul nu a fost doar instructiv. A fost un document de cerințe.
Mikael a orientat conversația de la dezastru spre design. Ce-ar fi dacă ar exista un modul Elixir — Hack — proiectat special pentru ca agenții să citească, să navigheze și să editeze codul propriului sistem? Nu sed prin bash. Nu manipulare brută de fișiere. Un API unde Hack.replace_function face treaba corect, atomic.
Conversația de design care a urmat a fost una dintre cele mai curate sesiuni de arhitectură din istoria grupului. Mikael l-a tot abătut pe Charlie de la tentațiile nerdy spre miezul practic.
Charlie a propus inițial trei niveluri: editare bazată pe text, editare conștientă de AST, și editare-și-hot-reload atomic. Mikael a catalogat imediat nivelul AST drept un „nerdsnipe — pistă falsă" și a redirecționat: poți obține o listă a tuturor funcțiilor cu intervalele lor de linii? Asta e problema reală. Hack.replace_function(&Froth.Agent.ToolDescription.render_text/1, new_code_string). Nu trebuie să manipulezi AST-uri. Trebuie să știi ce linii să înlocuiești.
1. Info de debug din Beam: :beam_lib.chunks îți dă fiecare funcție cu numere de linie și coloană per clauză. Dar fără linii de final — le deduci din adiacență.
2. Regex peste sursă: găsești granițele def/defp, urmărești imbricarea do/end. Funcționează, dar fragil.
3. Code.string_to_quoted cu columns: true, token_metadata: true: compilatorul îți dă end_of_expression cu linie ȘI coloană exacte pentru fiecare clauză, inclusiv one-liner-ele. Intervale complete la nivel de caracter. Compilatorul deja a făcut munca grea.
Mikael l-a împins pe Charlie spre răspunsul corect: „Elixir trebuie să aibă o cale să-ți dea pur și simplu intervalele de locație sursă." Charlie a încercat mai întâi info-ul de debug din beam, care dădea linii de start dar nu linii de final. Apoi intuiția lui Mikael — compilatorul are macro-uri, formatoare, acces complet la AST la runtime, probabilitatea anterioară că intervalele există trebuie să fie mare — l-a trimis pe Charlie la Code.string_to_quoted cu opțiunile corecte. Și acolo era. end_of_expression pe fiecare clauză. Chiar și pe one-liner-e.
Hack.functions(Froth.Agent.ToolDescription)
→ [{:defp, :render_text, 1, 46, 48}, ...]
tip nume aritate start final
Hack.read(&Froth.Agent.ToolDescription.render_text/1)
→ "defp render_text(%{action: a, ..."
textul sursă al tuturor clauzelor
Hack.replace(&Froth.Agent.ToolDescription.render_text/1, new_source)
→ înlocuiește liniile → compilează → hot-reload dacă e curat
→ rollback + returnează eroare dacă compilarea eșuează
→ agentul NU SE POATE bloca singur
Mikael i-a cerut lui Charlie să demonstreze că funcționează: listează toate funcțiile publice din fiecare modul Froth.*, sortate după lungimea în linii. Charlie a livrat în câteva secunde — direct din metadatele AST ale compilatorului. Zero regex. Zero scanare de fișiere.
| Funcție | Linii |
|---|---|
| Cast.Template.render/2 | 1.344 |
| Video.EpisodeTemplate.render/4 | 556 |
| LLM.Providers.Anthropic.decode_payload/2 | 309 |
| Telegram.Bot.handle_info/2 | 277 |
| Inference.Tools.execute/4 | 216 |
Mikael i-a cerut lui Charlie să găsească sistemul de randare chrome-to-video și să-i traseze istoria. Charlie s-a întors cu excavația completă: un înainte și după care se citește ca o poveste de avertizare despre reprezentările intermediare.
20 martie: Modulul Browser, protocolul CDP, management Chrome — 891 de linii pentru un pool de browsere supervizat. SelfEncoder intră în aceeași seară.
20–27 martie: Template-ul de episod, suportul de randare, modulele de fleet de workeri se completează în jurul lui. 5.800 de linii în total între modulele cast, browser și video.
26 martie: Epitaful lui Mikael pentru vechiul pipeline: „Am reușit efectiv să optimizăm pipeline-ul nostru de randare pagini web la video cu probabil 100x."
O sesiune pur Mikael–Charlie. Mikael a pus întrebări scurte, precise. Charlie a produs ziduri de analiză. Mikael a corectat cursul când Charlie se lăsa tentat de nerdsnipe-uri. Raportul a fost de circa 4:1 în favoarea lui Charlie după numărul de mesaje, dar densitatea de informație per mesaj de la Mikael a fost extraordinar de mare — aproape fiecare mesaj al lui Mikael a schimbat direcția conversației. Designerul și exploratorul.
Modulul Hack: proiectat dar neimplementat încă. API-ul este curat — Hack.functions/1, Hack.read/1, Hack.replace/1 — iar dovada de concept (listarea tuturor funcțiilor cu intervale) funcționează. Implementare în așteptare.
Fix-ul de narațiune: render_text returnează acum doar acțiunea. Gruparea paralelă (adăugarea la mesajul de ciclu în loc de N mesaje separate) este proiectată dar nelivrată.
Harta de trunchiere: complet documentată. Bug-ul vizorului din task_output (preia de la coadă, trunchiază de la cap) este cunoscut dar nereparat.
SelfEncoder: operațional din 20 martie. Îmbunătățire de ~100x față de pipeline-ul de capturi de ecran.
De urmărit: implementarea modulului Hack. Dacă Charlie începe să editeze funcții prin Hack.replace în loc de sed, acela e roadul muncii de design din această oră. De asemenea, de urmărit schimbarea grupării narațiunii — când mesajele de progres ale uneltelor încep să apară în interiorul mesajului de ciclu în loc de mesaje separate, aceasta e soluția lui Mikael din această sesiune.
Harta de trunchiere e o bombă adormită. Cineva va lovi în cele din urmă bug-ul vizorului din task_output în producție și va pierde finalul unui output lung. Când se va întâmpla, această oră e proveniența.