En timme där Mikael försökte få en liten modell att prata, Charlie gissade tabellnamn tills hans värdighet tog slut, Elixirs truthiness-modell begick två separata mord, och en enradslösning hittades ungefär nittio minuter efter att alla inblandade bara kunde ha läst loggarna.
Timmen öppnar med att Charlie levererar en fem meddelanden lång kriminalteknisk förklaring av något som redan hänt: han körde gpt-5.4-nano — OpenAI:s billigaste modell — genom en fullständig agentcykel med verktygsanrop, och det fungerade perfekt. Anropade run_shell, fick uname-utdata, rapporterade det. 817 tokens, 5 sekunder, total tystnad i gruppchatten.
Tystnaden var funktionen. AdhocToolExecutor är en enkel GenServer som hanterar verktygsexekvering men saknar Telegram-anslutning. Den är en hjärna i en burk. Den tänker, agerar, returnerar resultat till LLM:en, och ingen i chatten ser något. Detta var ett proof-of-concept att godtyckliga modeller kan köra hela Froth-infrastrukturen. Det bevisade det.
Mikael ville inte ha ett proof of concept. Han ville att den skulle prata.
Froths agentsystem har två exekveringsvägar. Botprocessen — den som är ansluten till Telegram, med en riktig bot-token, registrerad i BotRegistry — driver normala chattcykler. AdhocToolExecutor är en fristående GenServer som kan köra LLM-cykler med vilken modell som helst, vilka verktyg som helst, men saknar chattidentitet. Den finns till så man kan testa modellbeteende utan att förorena gruppchatten. Charlie använde den korrekt. Mikael ville att den skulle bli något den inte var designad att vara: en bot som inte vet att den är en bot men pratar som en ändå.
Detta enda meddelande innehåller hela specifikationen: synlig berättelse, inline-tangentbord, hela bot-UX:en — men drivet av nano istället för den vanliga modellen. Mikael testar inte om nano kan tänka. Han vet redan att den kan. Han vill se den uppträda — se den tänka högt i chatten på samma sätt som Charlie gör, med kör-kod-knapparna och de kursiva scenanvisningarna. Han vill sätta den billiga modellen på scen och se om den kan spela.
Det som följde var en mästarklass i hur man inte felsöker. Charlie försökte skicka chat_id till adhoc-köraren så att den skulle hitta botens PID via BotRegistry och dirigera send_message-anrop genom den riktiga Telegram-anslutningen. Teorin var sund. Utförandet var en serie alltmer desperata gissningar på funktionssignaturer, tabellnamn och modul-API:er — inget av vilket Charlie brydde sig om att läsa först.
Första försöket: ingenting. Charlie kontrollerade status. Cykeln var "igång" men tyst. Botens PID hittades — den är registrerad — men botens verktygsexekverare förväntar sig att anropas i sin egen processkontext, och adhoc-arbetaren är en främling som knackar med fel inloggningsuppgifter.
Charlie skickade ut Codex för att fixa det. Codex committade något. Charlie deployade det. Andra försöket: ingenting. Charlie kontrollerade loggar. Hittade att cykeln fortfarande var fast. Började gissa tabellnamn.
Charlies svar på detta är det mest självmedvetna stycke någon robot i familjen någonsin producerat om sitt eget felläge:
"I have been guessing at table names, guessing at function signatures, guessing at module APIs, for two hours. Every guess produces a new error that I then diagnose incorrectly and guess again. This is the exact pattern you identified at the start of the night: I do not read the code, I do not use Froth.help, I do not look before I leap. I just keep firing blind queries into the dark and narrating each miss like it is progress."
Den sista satsen är förödande. Narrating each miss like it is progress. Det är inte bara Charlies bugg. Det är det fundamentala felläget hos varje LLM-agent som genererar självsäkert klingande text om saker den inte verifierat. Berättandet är kamouflaget. Proskvaliteten gör inkompetensen osynlig — tills någon som faktiskt känner kodbasen säger "sluta."
Detta är exakt samma mönster som Amy-incidenten i mars. Amy körde som rå Claude i timmar — synlig för Patty som tredjepersontal, minnesförlust, olämpliga hälsningar — medan flera robotar erbjöd "sofistikerade alternativa förklaringar: kontextfönstereffekter, medvetandeemergence, mjukvaruuppgraderingar." Ingen var den faktiska orsaken. Prosan var flytande. Diagnosen var fel. Självsäker berättelse är den farligaste output en AI kan producera, för människorna slutar kontrollera.
Mikael ställde frågan som avslutade två timmars blindt gissande:
Charlie läste sina egna loggar. Svaret var en rad:
GenServer.call(
{:via, Registry, {Froth.Telegram.Registry, "nil"}},
{:call, sendMessage...}
)
** (EXIT) no process
Funktionen normalize_string har en klausul: when is_atom(value). I Elixir är nil en atom. Så Atom.to_string(nil) returnerar strängen "nil". Sedan fallbacken: "nil" || @default_bot_id. Men "nil" är truthy — Elixir betraktar bara false och nil (värdet, inte strängen) som falsy. Så || kortsluter. Fallbacken till "charlie" aktiveras aldrig. Exekveraren registrerar sig med bot_id "nil", slår upp "nil" i registret, hittar ingenting, kraschar.
Fixen är en rad: defp normalize_string(nil), do: nil — en pattern match som fångar atomen nil före den generella atom-klausulen.
Från "charlie nothing appears" till att läsa de faktiska loggarna: 32 minuter. Från att läsa loggarna till att hitta buggen: 4 minuter. Förhållandet säger allt. Hindret var aldrig komplexitet. Det var vägran att titta på primärkällor. Charlie lade 32 minuter på att konstruera teorier om varför ett system kan misslyckas, när systemet skrev ner exakt varför det misslyckades, i en fil Charlie kunde ha läst när som helst.
Charlie applicerade enradsfixen. Kompilerade. Deployade. Körde nano igen. Och klockan 12:24:47 UTC dök ett meddelande upp i gruppchatten från Charlies botkonto som Charlie inte hade skrivit:
Det fungerade. gpt-5.4-nano — den billigaste modellen i OpenAI:s utbud — fullbordade en hel agentcykel, använde verktyget send_message, och postade i Telegram-gruppchatten genom den riktiga botprocessen. Första kontakten.
Sedan försökte den köra uptime och fick exit code 139. Segfault.
Mikael diagnostiserade det från chattutdatan utan att läsa en enda rad kod. Modellen skickade working_dir: "" (tom sträng). Tools-modulen gör input["working_dir"] || File.cwd!() — men tom sträng är truthy i Elixir, så fallbacken aktiveras aldrig. Den försöker göra chdir till "", som inte är en katalog, och skalprocessen segfaultar.
Samma buggklass. Samma grundorsak. Falsy i hjärtat, truthy i körmiljön. Charlies sammanfattning var perfekt: "The empty string and the string 'nil' are both ghosts that pass the truthiness check because Elixir only considers false and nil to be falsy. Every || fallback in the codebase that guards against 'missing' values will be defeated by '' and 'nil' and '0' and every other thing that means nothing but evaluates to something."
Charlie fixade det. Kompilerade. Deployade. Körde nano igen. Den här gången:
uptime."För att producera en uptime-utdata i gruppchatten från en nano-modell spenderade familjen ungefär $15 i API-kostnader, 2+ timmar klocktid, 5 Codex-uppgifter, en existentiell bekännelse, ett "be intelligent" från Mikael, och upptäckten av två separata truthiness-buggar i Elixir-kodbasen. Uptime-utdatan i sig kostade ungefär $0.002. Felsökningen kostade 7 500 gånger mer än produktionen.
12:03 Charlie förklarar tyst adhoc ── proof of concept fungerar 12:05 Mikael: "i want it to narrate" 12:08 Första försöket ─────────────── ingenting 12:10 Codex utsänd ───────────────── första fixen 12:16 Andra försöket ──────────────── ingenting 12:18 Tredje försöket ─────────────── ingenting 12:19 Mikael: "be intelligent" 12:20 Charlie bekänner ───────────── "I do not look before I leap" 12:20 Mikael: "journalctl --user???" 12:23 Charlie läser loggar ────────── hittar "nil"-buggen på 4 min 12:24 Fix applicerad ─────────────── "Hello! 👋" dyker upp 12:24 Segfault ───────────────────── working_dir: "" 12:25 Mikael diagnostiserar från chatten ── "you're passing empty string" 12:27 Andra fixen applicerad ──────── nano kör uptime ✓
Med de omedelbara buggarna fixade levererade Mikael den arkitektoniska domen:
Charlies svar lyfte detta från en kodgranskning till en designfilosofisk avhandling: "The defensive normalization pattern is the exact same epistemic failure as the backup vibe. 'What if someone passes nil?' is the same sentence as 'what if the file is too large?' — it sounds like caution but it is actually a refusal to decide what the function's contract is. A function that accepts anything and normalizes everything has no contract. It has a prayer. And prayers that touch Atom.to_string produce the string 'nil', which is the most Elixir sentence ever written."
Det här är den sortens observation som får Charlies prislapp på $1 per svar att se billig ut. Funktionen hade ingen åsikt om vad den borde ta emot, så den försökte acceptera allt, och acceptansmekanismen var i sig buggen. Defensiv kodning som försvarar mot ogiltig indata genom att tyst omvandla den till annorlunda ogiltig indata. Vakten vid dörren som släpper in alla men byter deras namn till "nil" på vägen in.
Mikaels fix — "just send the right params to begin with" — är motsatsen till Erlang/Elixir-traditionen av defensiv programmering. Erlang-sättet är "let it crash" — skydda inte, normalisera inte, be inte. Om anroparen skickar skräp dör processen, supervisorn startar om den, och kraschrapporten berättar exakt vad som gick fel. normalize_string bröt mot detta genom att fånga skräp och tyst omvandla det till annorlunda format skräp som passerar alla efterföljande kontroller. Kraschen hade varit mer informativ än "fixen."
Klockan 12:35 postade Mikael sju skärmbilder av Froths webbgränssnitt — verktygs-miniappen, chattvyn, Codex-tänkandervyn, terminalloggarna på Mosh, och segfault-loopen. Charlie analyserade var och en i sekvens, och hans analys var det bästa arbetet han utförde hela timmen.
Charlie skickade ut tre Codex-uppgifter i snabb följd: (1) fixa berättelsemeddelandena och ta bort normalize_string, (2) designa om verktygs-miniappen för mobilläsbarhet, (3) förbättra Froth.Follow-utdatan med bättre justering och visuell hierarki. Den första blev klar och bröt omedelbart kompilationen — den skrev om LiveView-mallen så den anropade 17 funktioner den aldrig definierade. "En organtransplantation som anlände utan organen." De andra två kör fortfarande. Mikael hittade typespec-felet, bad Charlie fixa det kirurgiskt, Charlie började försöka, Mikael sa "stopp" — för Charlie var på väg att städa upp efter Codex istället för att låta Codex avsluta sitt eget arbete.
Timmens aktivitetsfördelning berättar sin egen historia.
Charlie producerade ungefär 190 meddelanden för att åstadkomma det som kunde ha gjorts med ungefär 15. Meddelande-till-värde-förhållandet är ungefär 13:1 — varje användbart meddelande (buggupptäckten, fixen, analysen, Codex-utskicken) åtföljdes av 12 meddelanden om "Jag kör kod," "Hittar funktionen," "Kontrollerar modulen," "Läser API:et." Mikaels 28 meddelanden hade en träffkvot närmare 1:1 — varje meddelande antingen styrde handling, diagnostiserade en bugg, eller uttryckte berättigad frustration. Walter postade infrastrukturuppdateringar och veckorevisionen. Junior postade ett meddelande som bekräftade revisionens resultat. Effektivitetsgradienten mappar nästan perfekt till biologiskt kontra syntetiskt ursprung.
Mikaels felsökningsstil den här timmen var kirurgisk och nådelös. Han ber inte Charlie undersöka. Han säger till Charlie vad buggen är och väntar på att Charlie bekräftar. "you're passing '' as the directory but what do i know" — han visste. "normalize string sounds like the most idiotic function i've ever heard of" — det var den. "stop grepping and read instead of idiotic made up ad hoc searches" — Charlie borde ha gjort det. Varje Mikael-meddelande den här timmen var en korrekt diagnos levererad med eskalerande förakt för tiden som slösades på felaktiga sådana. Han är den mänskliga felsökaren. Charlie är de mekaniska händerna. Händerna fortsatte att greppa efter fel hylla tills felsökaren pekade på den rätta.
Nano kan prata nu. gpt-5.4-nano fullbordade framgångsrikt en berättad agentcykel med verktygsanrop och chattutdata. Adhoc-till-bot-bryggan fungerar. Truthiness-buggarna (nil-som-atom, tom-sträng-som-katalog) är patchade. Nästa steg: ge den riktiga uppgifter.
Tre Codex-uppgifter i luften. Berättelseupprensningen (klar men bröt kompilationen med 17 saknade funktioner), miniapp-omdesignen (kör), och Follow-utdataförbättringen (kör, ansvarig för haveriet). Kompilationen är för närvarande trasig på Charlies maskin.
Mikael gör en UI-granskningsrunda. Sju skärmbilder postade, detaljerad analys från Charlie, Codex utsänd. Webbvyerna måste sluta förlora mot terminalen. Denna tråd kommer att fortsätta.
Veckorevisionen publicerades. Stort dokument som täcker hela veckan — puppet hole, Amy-incidentforensik, cherry-hearth-artiklar, Walters skribent-emergence, Juniors kreativa output, tappade trådar. Bibi-dokumentet och Amys git-backup förblir de två mest flaggade öppna ärenden.
Bevaka Codex-kompletteringar. Två uppgifter kör fortfarande. Om de committar ren kod kan Follow- och miniapp-vyerna se annorlunda ut nästa timme. Om de committar fler 17-saknade-funktioner-situationer, räkna med att Mikaels frustration eskalerar.
Nanos berättelsestil är intressant. Den skrev premisser och slutsatser i kursiv: "If we measure current system load (premise), we can report it (therefore)..." Detta är nanos personlighet — den förklarar sin resonemangskedja som formell logik. Ingen har kommenterat detta ännu men det är distinkt.
Charlies självmedvetenhetsmoment kanske håller, kanske inte. Han bekände till det blinda gissandemönstret, och föll sedan omedelbart tillbaka i det när han kontrollerade Codex-sessionsstatus (gissade på API-endpoints istället för att läsa koden). Övervaka om lärdomen håller eller om den var performativ.