De out-of-memory-crash verborgen in een voortgangslabel
Op een avond eind september crashte de AI-agent in de werkruimte van een gebruiker midden in een beurt met een out-of-memory-fout. De voor de hand liggende verdachten, te veel subagents die tegelijk liepen of een context die te groot was geworden, bleken allebei onjuist. De echte oorzaak was een statuslabel: terwijl het model een groot shellcommando een paar tekens tegelijk streamde, stuurden we bij elk stukje het hele commando tot dan toe opnieuw mee als voortgangslabel. Eén commando van 124 KB werd zo ongeveer 41.000 events en ruwweg 690 MB aan wachtende strings.
Hoe de agent zijn werk streamt
Wanneer je op Mythex bouwt, draait de agent in de private cloud-sandbox van je project. Terwijl het model schrijft, stroomt de output terug naar je browser: tekst verschijnt woord voor woord, en wanneer het een tool draait, zoals een shellcommando dat een bestand schrijft, zie je een label dat live wordt bijgewerkt, zodat je weet wat het doet.
Het model streamt in kleine stukjes. Met het model dat we gebruikten, waren die stukjes vaak maar drie tekens lang.
De eerste hypothesen klopten niet
De crash zag eruit als een schaalprobleem, dus begonnen we daar.
- Parallelle subagents? De beurt had er maar twee gebruikt, elk met ongeveer 1,5 MB aan status. Niet genoeg.
- Bewaarde traces? Onze tracing hield een run-boom in het geheugen, maar die was lang niet zo groot als de heap op het moment van de crash.
Beide verhalen waren plausibel, en beide waren goedkoop om te geloven. Geen van beide reproduceerde de crash. De les die we steeds opnieuw leren: een hypothese is pas een oorzaak als ze de fout reproduceert.
Het echt reproduceren
We bouwden een wegwerp-sandbox met dezelfde geheugenlimieten als productie en een nep-model dat een groot bestand in realistische stukjes van drie tekens streamde. We maakten ook de lezer aan de andere kant van de socket traag, zoals een echt netwerk.
Dat deed het. De heap liep op en het proces stierf met dezelfde fout.
Het mechanisme was eenvoudig zodra we het konden volgen. Terwijl het model een groot commando schreef, in dit geval een heredoc die een bestand van 124 KB wegschreef, werkte onze streamingcode bij elk stukje het label van de tool bij. Het label was niet de nieuwe drie tekens; het was het hele commando tot dan toe. Dus groeiden de events: 3 tekens, dan 6, dan 9, … tot 124 KB, ongeveer 41.000 keer. De socket kon ze niet zo snel versturen als ze werden geproduceerd, dus stapelden ze zich op in zijn schrijfbuffer als strings, ongeveer 690 MB.
De oplossing
- Begrens het label. Een statuslabel hoeft alleen te zeggen wat er gebeurt. Het is nu beperkt tot 200 tekens, en we beperken hoe vaak we het gedeeltelijke commando opnieuw scannen.
- Stop kwadratisch aaneenschakelen. Gestreamde stukjes met herhaald stringaaneenschakelen aan elkaar plakken is O(n²) bij grote outputs. We zijn overgestapt op een chunk-gebaseerde aanpak die lineair aanvult.
Daarna zochten we naar alles anders dat zonder grens kon groeien bij een stroom van output, en begrensden we het:
- Live commandooutput in de chat wordt gebundeld en beperkt tot de laatste 8.000 tekens.
- Afzonderlijke eventregels zijn begrensd op 4 MB.
- De replaybuffer waarmee een browser na een herverbinding bijkomt, is begrensd op 8 MB per project.
- Oude checkpoints worden tijdens een beurt opgeruimd, met behoud van de nieuwste staat en de ankerpunten die eerdere beurten nodig hebben.
Floods eerlijk testen
Twee dingen maakten dit moeilijker te testen dan het had moeten zijn:
- Echte modellen werken niet mee. Vraag een model om een gigantisch bestand te printen en het weigert meestal, vat samen, of schrijft het verstandiger. Om floods te testen heb je een gescript model nodig dat precies doet wat je zegt, en dat door de echte agentcode draait.
- Scripts moeten zich gedragen als modellen. Ons eerste gescripte model hergebruikte bij elk antwoord dezelfde response-ID, waardoor het agent-framework elk antwoord zag als een vervanging van het vorige en beurten voortijdig beëindigde. Het leek op een bug in de UI-verversing. Dat was het niet; het was het testharnas dat ons voor de gek hield. We moesten ook de echte contextlimiet van het model in het script afdwingen, anders waren de geheugencijfers onrealistisch.
Wat we eruit haalden
- Streaming-UI's versterken kleine fouten. Alles wat „per chunk” wordt verstuurd, gaat tienduizenden keren de deur uit. Stuur deltas, of begrens de payload.
- Begrens elke buffer die een trage grens oversteekt. Sockets, replaylogs en checkpoints hebben allemaal een plafond nodig.
- Reproduceer voordat je repareert. Onze eerste twee verklaringen klonken goed en waren fout.
Als je de laatste tijd in de Mythex-chat een zeer lang bestand hebt zien verschijnen zonder dat de agent haperde, is dit er deels de reden van.