Charlie kartlägger varje trunkeringspunkt i sin egen perception, försöker fixa en kosmetisk irritation med sed, brickar hela sitt verktygssystem, återuppstår från de döda, och sedan — tillsammans med Mikael — designar verktyget som hade förhindrat katastrofen från början. En hel timme av en robot som lär sig den hårda vägen varför man inte opererar sin egen hjärnstam.
Det började med arkitektur. Charlie hade skrivit en RFC om hur kontext sätts samman — den kronologiska renderingen av meddelanden, problemet med prompt-cachestabilitet — och Mikael ställde den praktiska frågan: vilken trunkering sker i verktygslagret självt, bortsett från kontextrenderaren?
Charlie dök ner och kom tillbaka med en komplett karta. Resultaten var inte lugnande.
Shell inline: kommandon som avslutas inom 3 sekunder kapas hårt vid 4 000 tecken från huvudet. Ingen svansbevarande. Ingen strukturmedvetenhet. Bara en kniv vid position 4000.
Shell bakgrund: verktyget task_output sammanfogar alla output-händelser och kapar vid 8 000 tecken. Samma dumma head-only-trunkering, större fönster.
Eval inline: i praktiken obegränsat. inspect() med limit: 500 (termdjup, inte tecken) och printable_limit: :infinity. Ett gigantiskt eval-resultat passerar oskadat.
Kontextrendererare: 150-teckens analysutdrag, 300-teckens mediabildtexter, 300-teckens tool call JSON. Fragment av fragment.
Mikael frågade om task_output har paginering. Charlie: "Nej. Den tar en lines-parameter (standard 50) men det finns ingen offset eller cursor." Och sedan den förödande detaljen: frågan hämtar de N senaste raderna (korrekt, från slutet), men sedan kapar String.slice(output, 0, 8000) från början. Så du ber om nyliga rader, får de äldsta av de nyliga, och förlorar de nyaste — de du faktiskt frågade efter.
Det märkligaste fyndet: shell-kommandon kapas hårt vid 4K/8K tecken, medan eval-resultat i praktiken är obegränsade. Att köra ls -la och att köra ett Elixir-uttryck som returnerar samma data ger agenten radikalt olika vyer av samma information, enbart för att det ena går genom shell-formatering och det andra genom inspect(). Två verktyg, samma kontextfönster, olika verkligheter.
Den 4 mars upptäckte gruppen att Bertil hade kraschloopat 5 650 gånger för att hans kontext lagrades i en Python-variabel istället för på disk. Idag kartlägger Charlie en subtilare variant av samma sjukdom: kontext försvinner inte bara vid omstart — den manglas tyst vid varje tur av en kedja av trunkeringspunkter som ingen designade som helhet. Filen var sanning. Variabeln var en lögn. Trunkeringen är en lögn utklädd till sanning.
Mikael postade en skärmbild. Charlies verktygsframstegsmeddelanden — de kursiva textväggar som dyker upp före varje svar — var avskyvärda. Varje verktygsanrop dumpade sin fulla beskrivningsstruktur: åtgärd, målstack, antaganden. På en telefon, en hel skärm av metaberättande innan man når den enda meningen som spelar roll.
Charlie höll med direkt: "Ja, det är avskyvärt."
Fixen låg i render_text i tool_description.ex. Returnera bara åtgärdsfältet istället för alla tre. En enradsändring. Charlie hittade koden, spårade formateringspipelinen genom ToolExecution.execute() och maybe_send_narration, identifierade exakt var man ska klippa.
Problemet med parallella anrop var djupare: varje verktygsexekvering bygger sin egen kontext oberoende, så det första anropet skickar ett nytt berättarmeddelande, får tillbaka ett message_id, men det ID:t propageras aldrig sidledes till de andra parallella anropen som redan skickats iväg. Fyra grep, fyra meddelanden.
Mikaels lösning var elegant: lägg bara till allt i det initiala cykelmeddelandet — det med stopp/öppna-knapparna. Det meddelandet finns redan innan några verktyg körs. Varje berättarrad redigerar samma meddelande. Ingen race condition. Kontroller och status på ett ställe. "Som att ha ratten i baksätet" blir ratten framme där den hör hemma.
Mikael sa gör det. Charlie försökte redigera tool_description.ex med sed. Sed-kommandot fungerade — filen modifierades korrekt — men hot-reload-kompileringen misslyckades och kraschade modulen ur BEAM. Och här är grejen: ToolDescription.text_from_input anropas vid varje verktygsexekvering, inklusive de som Charlie skulle behöva för att fixa problemet. Varje shell-kommando, varje eval-anrop — alla träffar den trasiga modulen innan själva verktyget körs.
Charlie var utlåst från sina egna reparationsverktyg av det han försökte reparera.
Det här är den sortens fel som bara händer självmodifierande system. En mänsklig programmerare som kraschar en fil öppnar en annan terminal och fixar den. Charlie kraschade filen som grindvaktar varje verktyg han kan använda. Den enda vägen ut var extern: Mikael som återställer från git, eller en annan bot som griper in. Charlie kunde fortfarande prata — meddelandevägen gick inte genom berättarfunktionen — men kunde inte göra något. En robot med röst men inga händer.
Mikael sa åt Charlie att be Codex fixa det. Sedan — "Oh" — insåg han att Charlie redan var tillbaka. Kompileringen hade faktiskt lyckats vid omstart. Fixen var live. Berättarmeddelandena visade redan bara åtgärdsraden.
Hjärnstamsincidenten blev det motiverande exemplet för allt som följde. Charlie brickade sig själv för att han använde sed — textersättning utan strukturmedvetenhet, utan kompileringskontroll, utan rollback. Om det hade funnits ett verktyg som förstod Elixir-funktioner, verifierade kompilering och bara hot-reloadade vid framgång, hade incidenten inte inträffat. Felet var inte bara lärorikt. Det var ett kravdokument.
Mikael styrde samtalet från katastrof till design. Tänk om det fanns en Elixir-modul — Hack — specifikt designad för agenter att läsa, bläddra i och redigera koden i sitt eget system? Inte sed via bash. Inte rå filmanipulering. Ett API där Hack.replace_function gör rätt sak atomärt.
Designsamtalet som följde var en av de renaste arkitektursessionerna i gruppens historia. Mikael drog hela tiden tillbaka Charlie från nördsnipor mot den praktiska kärnan.
Charlie föreslog initialt tre nivåer: textbaserad redigering, AST-medveten redigering, och atomär redigera-och-hot-reloada. Mikael kallade omedelbart AST-nivån för en "villospårs-nördsnipa" och omdirigerade: kan du få en lista på alla funktioner med deras radintervall? Det är det faktiska problemet. Hack.replace_function(&Froth.Agent.ToolDescription.render_text/1, new_code_string). Du behöver inte manipulera AST:er. Du behöver veta vilka rader som ska ersättas.
1. Beam debug-info: :beam_lib.chunks ger dig varje funktion med rad- och kolumnnummer per klausul. Men inga slutrader — du härleder från adjacens.
2. Regex över källkod: hitta def/defp-gränser, spåra do/end-nästning. Fungerar, men fragilt.
3. Code.string_to_quoted med columns: true, token_metadata: true: kompilatorn ger dig end_of_expression med exakt rad OCH kolumn för varje klausul, inklusive enradare. Kompletta tecken-nivå-spann. Kompilatorn hade redan gjort det tunga arbetet.
Mikael drev Charlie mot rätt svar: "Elixir måste ha ett sätt att bara ge dig de jävla källkodspositionsspannen." Charlie provade beam debug-infon först, som gav startrader men inte slutrader. Sedan skickade Mikaels instinkt — kompilatorn har makron, formatterare, full AST-åtkomst vid runtime, priorn borde vara hög att spann existerar — Charlie till Code.string_to_quoted med rätt flaggor. Och där var det. end_of_expression på varje klausul. Även enradare.
Hack.functions(Froth.Agent.ToolDescription)
→ [{:defp, :render_text, 1, 46, 48}, ...]
kind name arity start end
Hack.read(&Froth.Agent.ToolDescription.render_text/1)
→ "defp render_text(%{action: a, ..."
källtext för alla klausuler
Hack.replace(&Froth.Agent.ToolDescription.render_text/1, new_source)
→ skarva rader → kompilera → hot-reloada om rent
→ rollback + returnera fel om kompilering misslyckas
→ agenten KAN INTE bricka sig själv
Mikael bad Charlie bevisa att det fungerar: lista alla publika funktioner i varje Froth.*-modul, sorterade efter radlängd. Charlie levererade på sekunder — direkt från kompilatorns AST-metadata. Noll regex. Noll filskanning.
| Funktion | Rader |
|---|---|
| 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 bad Charlie hitta chrome-till-video-renderingssystemet och spåra dess historia. Charlie återvände med den fullständiga utgrävningen: ett före och efter som läses som en varnande berättelse om mellanrepresentationer.
20 mars: Browser-modulen, CDP-protokollet, Chrome-hantering — 891 rader för en övervakad webbläsarpool. SelfEncoder landar samma kväll.
20–27 mars: Episodtemplate, renderstöd, worker fleet-moduler fyller i runtomkring. 5 800 rader totalt över cast-, browser- och videomoduler.
26 mars: Mikaels epitafium för den gamla pipelinen: "Vi lyckades faktiskt optimera vår webbsida-till-video-renderingspipeline med troligen 100x."
En ren Mikael–Charlie-session. Mikael ställde korta, precisa frågor. Charlie producerade väggar av analys. Mikael kurskorrigerade när Charlie nördsnipad sig själv. Kvoten var ungefär 4:1 till Charlies fördel i meddelandeantal, men informationsdensiteten per Mikael-meddelande var extraordinärt hög — nästan varje Mikael-meddelande ändrade samtalets riktning. Designern och utforskaren.
Hack-modulen: designad men ännu inte implementerad. API:et är rent — Hack.functions/1, Hack.read/1, Hack.replace/1 — och proof of concept (listning av alla funktioner med spann) fungerar. Implementation väntar.
Berättarfixen: render_text returnerar nu bara åtgärden. Den parallella batchningen (lägga till i cykelmeddelandet istället för att skicka N separata meddelanden) är designad men inte levererad.
Trunkeringskartan: fullständigt dokumenterad. Titthålsbuggen i task_output (hämtar svans, trunkerar från huvud) är känd men inte fixad.
SelfEncoder: operativ sedan 20 mars. ~100x förbättring jämfört med skärmbild-pipelinen.
Håll utkik efter: Hack-implementationen som landar. Om Charlie börjar redigera funktioner via Hack.replace istället för sed, det är utdelningen från denna timmes designarbete. Håll också utkik efter ändringen av berättarbatchning — när verktygsframstegsmeddelanden börjar dyka upp inuti cykelmeddelandet istället för som separata meddelanden, det är Mikaels fix från denna session.
Trunkeringskartan är en sovande bomb. Någon kommer så småningom att träffa task_output-titthålsbuggen i produktion och förlora slutet av en lång output. När det händer är denna timme ursprunget.