Die Suche, die ich befohlen habe, ohne es zu merken
Sven Pöche · Senior Software Engineer 3. November 2026 23 Min. Lesezeit Development

Die Suche, die ich befohlen habe, ohne es zu merken

Ein rsync, der nicht vorankommt, führt zu einer falschen Diagnose, einer richtigen Handlung aus dem falschen Grund und schließlich zu sieben Vollscans quer über die Platte, die meine eigenen Subagenten in der Nacht gestartet hatten. Vier von achtundzwanzig Aufträgen nannten einen Ort.

Blog » Development » Die Suche, die ich befohlen habe, ohne es zu merken

Der Transfer läuft seit zwanzig Minuten und hat vierundzwanzig Megabyte geschafft. Nicht pro Sekunde, sondern insgesamt. Ein rsync von meiner Platte auf einen Rechner im Heimnetz. So ein Tempo muss man nicht ausrechnen, man sieht es.

In der Nacht davor war derselbe Lauf nach drei Stunden neunzehn Minuten abgebrochen, bei achtundzwanzig Prozent der zu übertragenden Datenmenge. An dieser Stelle steht im Log ein Lesefehler: Operation timed out, darunter ein Fehler in der Socket-Kommunikation. Danach stand neuneinhalb Stunden gar nichts: kein Wiederanlauf, kein Byte der fehlenden zweiundsiebzig Prozent, keine Meldung. Ein Backup, das unbeaufsichtigt durchlaufen soll und bei gut einem Viertel steht, ist kein Backup, sondern eine offene Frage.

Also starte ich neu. Es ist dieser Neustart, der jetzt seit zwanzig Minuten läuft; die Zahlen vom Anfang stammen aus ihm, nicht aus dem nächtlichen Lauf. Ich sehe zu, wie sie kriechen, und mache das, was man in so einem Fall macht: Ich lasse nachsehen, wer die Leitung frisst. In der Prozessliste des Zielrechners stehen zwei Prozesse ganz oben (beide gehören zu smbd, dem Dienst, der dort die Netzfreigaben ausliefert), und die Last ist nicht Rechenlast, sondern Ein- und Ausgabe. Irgendetwas auf meiner Seite schiebt Arbeit auf diese Maschine. Den Verdacht, was es sein könnte, hatte allerdings nicht ich.

Die naheliegende Erklärung …

Der Verdächtige hieß Time Machine. Sie läuft überhaupt erst seit einem Datenverlust, den ich mir selbst zugezogen habe: damals gab es sie nicht, genau das war die Lehre. Seither sichert sie stündlich über SMB auf einen Rechner im Heimnetz, und sie war gerade unterwegs, auf denselben Rechner, auf den auch der rsync schrieb.

Das ist eine gute Hypothese, und sie ist es aus mehr als einem Grund. Zwei Dienste, die über dieselbe Leitung auf dieselbe Kiste schreiben, behindern einander. Einer davon arbeitet mit sehr vielen kleinen Dateien statt mit wenigen großen. Und die Last auf dem Zielrechner passte dazu: Ein- und Ausgabe statt Rechenlast – genau das, was viele kleine Dateien erzeugen. Wenn du eine Signatur suchst, die zu einem stündlichen Sicherungslauf über ein Netzlaufwerk passt, dann ist das diese. Dazu kommt die Bequemlichkeit: Time Machine war der einzige Verdächtige, den ich mit einem Klick prüfen konnte. Und ich debuggte den Transfer nicht allein, sondern in einer Sitzung.

„Sitzung“ heißt hier durchgehend: ein laufender Dialog mit Claude Code, dem Agenten-Werkzeug, mit dem ich in der Kommandozeile arbeite. Sie liest mit, führt Kommandos aus und schreibt ein vollständiges Protokoll mit: jede Nachricht, jedes Kommando, jede Ausgabe. Aus diesen Protokollen stammt alles, was hier zitiert wird. Diese eine nenne ich die Transfer-Sitzung; sie ist nicht die einzige, die an diesem Tag lief.

Gefasst hat sie ihn, nicht ich

In der Liste der Netzfreigaben fand sie neben dem rsync-Ziel einen zweiten Mount auf demselben Rechner und schrieb dazu: „Da ist der Verursacher.“ Dann prüfte sie selbst nach, ob die Sicherung gerade lief. Zweiundzwanzig Sekunden später stand da: „Gefunden. Time Machine sichert gerade parallel auf dieselbe Synology.“ Samt Phase, Fortschritt und geschätzter Restzeit. Ich habe das gelesen und für richtig gehalten.

Sie schlug eine kurze Liste von Maßnahmen vor, obenan das Anhalten der Sicherung. Ich habe Time Machine abgeschaltet, tmutil status meldete Running = 0, und damit war der Verdächtige aus dem Spiel. Die Systemlast auf dem Zielrechner fiel daraufhin tatsächlich. smbd nicht: Der Dienst stand danach höher als vorher und weiter ganz oben in der Liste.

Das ist der Moment, an dem eine Diagnose eigentlich zu Ende ist. Die vermutete Ursache war weg, das Symptom war es nicht. Genau das hat die Transfer-Sitzung sogar ausgeschrieben, in einem Nebensatz mitten in einem Absatz über die nächsten Schritte: Die Systemlast sei gesunken, smbd stehe aber weiter an der Spitze. Und ging dann darüber hinweg. Ich auch.

Wir machten dieselbe Bewegung: Erklärung gefunden, Erklärung entfernt, Ergebnis nicht geprüft, und weitergearbeitet an der Maßnahmenliste, die aus der falschen Erklärung entstanden war.

Ein Nebenrat, der sich nicht ausführen ließ

Auf dieser Liste stand noch etwas: Neben dem Hauptvorschlag hatte die Transfer-Sitzung beiläufig geraten, das rsync-Ziel auszuhängen (/Volumes/backups), ausdrücklich als Zusatz und nicht als Hebel. Es ging dabei auch nicht um den Time-Machine-Share; das sind zwei verschiedene Freigaben auf demselben Rechner.

Das Aushängen ging nicht:

umount(/Volumes/backups): Resource busy -- try 'diskutil unmount'

Für sich genommen sagt das wenig: Bei SMB-Mounts ist ein Resource busy fast die Regel. Der nächste Schritt ist trotzdem Routine – fuser fragen, welcher Prozess die Freigabe hält. Nur nannte fuser keinen Halter. Es blieb selbst an einem anderen Netzwerk-Mount hängen, nämlich dem, auf den Time Machine sichert, und warnte statt eines Prozessnamens, dass es dieses Dateisystem nicht abfragen konnte. Das Werkzeug scheiterte damit an einer Eigenschaft von Netzlaufwerken, die gleich noch einmal wichtig wird: Ein Zugriff auf eine Netzfreigabe scheitert nicht, wenn das Gegenüber nicht antwortet; er wartet. Die Transfer-Sitzung hielt fest: „Der Verursacher ist damit nicht gefunden.“

An dieser Stelle greift bei mir eine Erfahrungsregel, und es ist genau das: Erfahrung, kein Messwert. Ein Terminalfenster kann nach dem Arbeiten noch offene Dateien halten, und ein Mount geht deshalb manchmal einfach wieder frei, wenn man aufräumt. Also ließ ich die laufenden Claude-Code-Sitzungen anhalten und fing an, sie zu beenden. Die Regel führte zur richtigen Handlung – richtiger, als ich in diesem Moment wissen konnte.

Zwischen dem gescheiterten Aushängen und dem, was sich gleich darauf fand, liegen achtundzwanzig Minuten – eine knappe halbe Stunde, in der niemand hinsah.

Zweiundfünfzig Sekunden

Kurz nach elf tippte ich /exit – nicht in der Transfer-Sitzung, sondern in einer zweiten, die seit dem Vortag lief: der Sitzung, mit der ich an einem privaten Modding-Projekt arbeitete. Sie heißt hier die Projekt-Sitzung, und mit dem Transfer hatte sie nichts zu tun außer der Maschine, auf der beide liefen.

Beim Verlassen meldet Claude Code laufende Hintergrundarbeit, mitsamt einer Aufzählung dessen, was noch offen ist. Sie war völlig gewöhnlich, und was im Einzelnen dort stand, kann ich nicht mehr sagen. Ich lese diese Liste meistens nicht; der Wortlaut steht auch in keinem der beiden Protokolle. Das /exit stößt das Beenden ohnehin nur an: Solange die Meldung steht, ist die Sitzung noch da, und man kann in ihr weiterschreiben. Also tat ich, was ich in solchen Fällen immer tue: Ich fragte die Sitzung selbst, was da noch lief.

Neun Sekunden nach dem /exit schrieb ich in die Projekt-Sitzung: „Es laufen noch jede Menge Shells.“ Das ist keine Diagnose. Das ist der Satz, den man tippt, wenn einem etwas komisch vorkommt und man keine Lust hat, es selbst nachzusehen.

Wem gehören die sieben Prozesse?

Vier Sekunden später sah die Projekt-Sitzung nach – sie lief ja noch. Sieben Prozesse namens bfs, der älteste über zwölf Stunden alt. bfs steht dabei dort, wo man find erwartet: Was als find losgeschickt wurde, erscheint in der Prozessliste unter diesem Namen. Und bevor sie irgendetwas beendete, stellte sie die Frage, an der alles Weitere hängt (das „ich“ darin ist ihres, nicht meins):

„Sieben bfs-Prozesse, teils über zwölf Stunden alt. Bevor ich irgendetwas beende, muss ich wissen, ob sie überhaupt meine sind; es laufen mehrere Claude-Sessions auf dieser Maschine.“

Das ist keine rhetorische Vorsicht. Auf diesem Rechner laufen regelmäßig mehrere Sitzungen parallel – meine eigenen, in verschiedenen Fenstern, an verschiedenen Projekten. Einen Prozess abzuschießen, weil er im Weg steht, trifft dann womöglich die Arbeit von nebenan. Die Projekt-Sitzung prüfte also, an welcher Shell die sieben Prozesse hingen. Eine halbe Minute nach dem /exit stand die Antwort da. Was sie dazu schrieb, nennt zwei Dinge, die den Rest dieses Artikels tragen: Subagenten und CoreLib. Subagenten sind eigenständige Agenten, die eine Sitzung für eine Teilaufgabe losschickt. Sie arbeiten mit denselben Werkzeugen wie sie. CoreLib ist die fremde Bibliothek, gegen die in jenem Projekt gebaut wird und deren Quelltext die Subagenten prüfen sollten. Wenn sie gleich „meine Subagenten“ schreibt, sind ihre eigenen gemeint:

„Jetzt ist die Herkunft klar … und es ist meine Verantwortung. Alle sieben sind find /-Vollscans meiner Subagenten, die nach CoreLibs Quelldateien suchen.“

Alle aus derselben Sitzung

Alle sieben hingen an derselben Shell-Umgebung: der der Projekt-Sitzung. Das ist der Beleg, den die Frage nach der Herkunft braucht. Jede Sitzung legt sich beim Start ihre eigene an, und jeder Prozess, den sie oder einer ihrer Subagenten startet, hängt daran. Es waren verwaiste Vollscans ihrer eigenen Subagenten, die meisten in der Nacht gestartet, längst vergessen von den Agenten, die sie losgeschickt hatten. Sechs Sekunden später waren sie beendet.

Und damit hätte die Geschichte hier aufhören können.

Elf Sekunden danach lief ein achter. Zehn Sekunden alt, wieder ein Vollscan ab /. Die Sitzung erkannte ihn zunächst als fremd und schrieb: „Ein neuer Vollscan ist gestartet, den ich nicht ausgelöst habe.“ Dann prüfte sie auch bei ihm die Herkunft. Das Ergebnis kam eine Sekunde später und stand in Großbuchstaben da: AUS DIESER SESSION.

Während aufgeräumt wurde, startete der nächste Scan, und er stammte aus derselben Sitzung, die gerade aufräumte. Sie hat ihn nicht kommen sehen, sie hat ihn für fremd gehalten, und sie musste erst nachsehen, um sich selbst zu erkennen. Zwischen dem, was eine Sitzung von ihrer eigenen Hintergrundarbeit weiß, und dem, was von ihr tatsächlich noch läuft, klafft also eine Lücke, und sie steht im Protokoll selbst, in den beiden Sätzen von eben.

Zweiundfünfzig Sekunden, und kein Werkzeug hat die Verbindung hergestellt zwischen den Prozessen, die seit Stunden liefen, und der Sitzung, die sie losgeschickt hatte. Diese Verbindung entstand, weil jemand nachfragte.

Der Mount ging wieder frei

Und nebenbei beantwortete sich die Frage vom Vormittag. Nachdem die Scans weg waren, ließ sich /Volumes/backups ohne Weiteres aushängen. Zwei Minuten nach ihrem Ende taucht es in keiner Mount-Liste mehr auf. Bewiesen ist damit nichts: fuser hatte nie einen Halter genannt, und zeitliche Nähe ist keine Ursache. Aber der einzige Eingriff dazwischen war das Beenden der sieben. Es ist derselbe Handgriff wie bei Time Machine: Verdächtigen entfernen und nachsehen, was das Symptom macht. Dort blieb es und widerlegte die Erklärung; hier verschwand es. Der Prozess, der das Aufräumen blockierte, war aller Wahrscheinlichkeit nach einer von denen, die dieses Aufräumen zutage förderte.

Was da lief

Sieben Prozesse, jeder ein find ab /. Der kürzeste lief vierunddreißig Minuten, der längste zwölf Stunden neunundvierzig Minuten; dieser längste war am Vorabend gestartet worden.

Unter diesen sieben steckten vier verschiedene Suchmuster: drei davon suchten nach einem Dateinamen, das vierte nach einem Pfadmuster quer über die gesamte Platte. ck.modding.corelib ist dabei der Paketname, unter dem CoreLib installiert wird – eine Quelltext-Mod, die ihre .cs-Dateien mitliefert statt nur das fertig Übersetzte. Genau deshalb kann ihr Quelltext später an mehr als einer Stelle auf der Platte liegen. Der achte Scan, der während des Aufräumens startete, brachte ein fünftes mit.

Diese fünf Muster sind der Maßstab für das, was unmittelbar auf den Fund folgte. Nachdem die Herkunft der sieben Scans geklärt war, schrieb sich die Projekt-Sitzung eine Regel dagegen auf, in eine Notizdatei, die sie über das Ende dieser Sitzung hinaus behält; ich nenne sie hier die Regel-Notiz. Sie kennt nur vier der fünf Muster und unterschlägt ausgerechnet das Pfadmuster -path *ck.modding.corelib*ConfigFile.cs. Das ist der erste von zwei Fehlern dieser Notiz.

Ein Scan, der das Gerät verlässt

Zusammen kamen die sieben auf rund 12 %, die Summe der Werte, die ein einzelner ps-Aufruf in seiner CPU-Spalte für sie auswies, eine Momentaufnahme und keine Messreihe. Die Rechenlast ist ohnehin nicht der Punkt. Der Punkt ist, was ein Scan ab / auf dieser Maschine tatsächlich anfasst.

Unter /Volumes hingen acht SMB-Freigaben von zwei Rechnern im Heimnetz. Ein find / interessiert sich nicht dafür, dass hinter einem Verzeichnis ein Netzwerk steckt. Es steigt hinein und zählt Einträge, genau wie überall sonst. Eine der acht Freigaben ist das Ziel der stündlichen Time-Machine-Sicherung von vorhin, und sie ist der Extremfall; die Projekt-Sitzung beschrieb sie im Protokoll so: „Millionen hardlink-verketteter Einträge, jeder Verzeichniswechsel eine Netzwerkrunde.“ Und wenn so ein Mount schläft, hängt der Scan – er wartet, bis das Gegenüber wieder antwortet, und läuft dann weiter, als wäre nichts gewesen.

Deshalb ist ein Vollscan auf so einer Maschine kein lokaler Vorgang. Er verlässt das Gerät. Und er tut es siebenfach parallel, über Stunden, auf Rechner, die für ganz andere Aufgaben eingerichtet sind.

Warum es über Nacht niemandem auffiel

Bleibt die Frage, warum das über eine ganze Nacht niemandem aufgefallen ist, auch den Agenten nicht, die die Scans gestartet hatten. Die Antwort steckt nicht im Betriebssystem, sondern eine Ebene darüber, bei Claude Code selbst: Ein Bash-Aufruf, der nach 120 Sekunden nicht fertig ist, wird dort in den Hintergrund verschoben. Der Agent bekommt die Kontrolle zurück. Er arbeitet weiter, meldet fertig, wird beendet – und der Prozess läuft. Genau sieben solche Ereignisse stehen in den Protokollen der Subagenten, eines für jeden der sieben Prozesse vom Vormittag. Der achte Scan, der erst beim Aufräumen startete, ist darin nicht enthalten. Kein Agent hat hier etwas ignoriert; jeder hat eine Meldung bekommen, die er als erledigt verbuchen durfte.

Einer der Agenten hat sogar versucht aufzuräumen. Er schickte ein pkill auf seinen eigenen Scan und bekam done zurück. Der Prozess lief weiter, bis er am nächsten Tag mit den anderen beendet wurde, elf Stunden neunundzwanzig Minuten alt. Ein Aufräumversuch hat also existiert, hat Vollzug gemeldet und hatte keine Wirkung.

Der Transfer, nach den Scans

Nachdem die Scans weg waren, kam der Transfer knapp dreimal so schnell voran wie zu Beginn dieses Artikels. Das ist eine Größenordnung, kein Laborwert: Die beiden Ablesungen stammen aus zwei verschiedenen rsync-Prozessen. Der Vormittagslauf wurde gut zwei Minuten nach der ersten Ablesung durch ein Abbruchsignal beendet, der nächste lief erst nach dem Ende der Scans an; von ihm stammt die zweite Ablesung, siebenunddreißig Minuten nach der ersten.

Was den Vergleich trotzdem trägt, ist alles außer der Prozessgleichheit: dasselbe Vorhaben, dieselbe Quelle, dasselbe Ziel, derselbe Vormittag, und beide Ablesungen fallen in dieselbe Phase, in der rsync noch die Dateiliste aufbaut. Was er nicht trägt: Zwei verschiedene Prozesse in unterschiedlich langen Fenstern sind kein wiederholter Versuch unter gleichen Bedingungen, und über den Transfer danach sagt er ohnehin nichts; nur, dass es ohne die Scans deutlich schneller voranging. Für die Frage, mit der der Tag angefangen hat, reicht das trotzdem. Die vierundzwanzig Megabyte in zwanzig Minuten waren nicht das Wetter. Sie waren die Rechnung.

Das Nachzählen

Dieselbe Regel-Notiz enthielt den zweiten Fehler. In ihr stand, die Prompts hätten den Pfad zum Quelltext von CoreLib jedes Mal mitgegeben. (Prompts, das sind hier die Aufträge, mit denen die Projekt-Sitzung ihre Subagenten losschickte.) Die Agenten hätten eben trotzdem gesucht.

Ich habe gefragt. Das Dekompilat, von dem ich dabei spreche, ist der aus dem Spiel zurückgewonnene Quelltext, ein eigener Checkout auf meiner Platte. CoreLibs Quelltext liegt dort als eigenes Projekt daneben, und zwar im Original; eine Quelltext-Mod muss man nicht dekompilieren. Der umgebende Ordner heißt trotzdem nach dem Dekompilat, und so nenne ich ihn auch:

„Wieso wurde überhaupt ein Vollscan gemacht? Das Verzeichnis des Dekompilats ist doch bekannt.“

Die Projekt-Sitzung hat die eigene Behauptung nicht verteidigt, sondern nachgezählt: alle Subagenten-Aufträge der Sitzung durchgehen und sehen, in wie vielen ein Ort für CoreLibs Quelltext tatsächlich stand. Der Befehl steht im Protokoll, das Ergebnis auch.

4 von 28.

Was die Zahl misst und was nicht

Zur Zahl gehören zwei Dinge. Erstens die Bezugsgröße: Beim Zählen lief die Projekt-Sitzung noch, und in dem Moment waren es 23 Aufträge. Danach kamen fünf weitere, keiner davon nannte einen Ort. Über die abgeschlossene Sitzung sind es 28. Zweitens der Kontext, ohne den die Quote etwas anderes behauptet, als sie misst: In jenem Projekt ist subagentengetriebenes Arbeiten der Standardmodus. Achtundzwanzig Aufträge in einer Sitzung, die knapp einundzwanzig Stunden lief, sind dort Normalbetrieb und keine Häufung. Die Quote misst ein Verfahren, nicht einen Ausreißer.

Die Projekt-Sitzung nannte das Ergebnis „unbequem“: es „widerlegt meine eigene Memory-Notiz von vorhin“; „Memory-Notiz“ ist dabei ihr eigener Name für die Regel-Notiz. Auch die Transfer-Sitzung hat ihren eigenen Fehler benannt, als alles vorbei war (auch dieses „ich“ gehört ihr): „Das Symptom blieb, nachdem ich die vermutete Ursache abgeschaltet hatte — genau dort hätte ich weitersuchen müssen.“ Der Satz stimmt. Nur hilft er an der Stelle, an der er fällt, niemandem mehr.

Mit der Zahl dreht sich die Schuldfrage

Mit der Zahl dreht sich die Schuldfrage. Sechs der sieben Scans stammen aus Aufträgen ohne Quellpfad. In der großen Mehrheit der vierundzwanzig Aufträge ohne Ort stand eine Behauptung über CoreLib, die geprüft werden sollte, und kein Ort, an dem man sie hätte prüfen können. Anders als die Quote oben ist dieser Anteil allerdings nicht ausgezählt, sondern eine Lesart der Auftragstexte.

Der Agent war nicht fahrlässig. Er war gehorsam. Wer eine Verifikation verlangt und keinen Ort nennt, hat keine Prüfung beauftragt, sondern eine Suche.

An dieser Stelle gehört eine Einschränkung hin, die im Titel untergeht. Getippt habe ich keinen dieser achtundzwanzig Aufträge. Formuliert hat sie die Projekt-Sitzung, und sie sagt das im Protokoll auch selbst. Was ich getippt habe, war die Aufgabe darüber. Und die Arbeitsweise, in der so etwas an Subagenten weitergereicht wird, ist meine. Nicht im Sinne einer Gewohnheit, die sich eingeschlichen hat: In einer Notiz zu diesem Projekt steht ausdrücklich, dass Code-Arbeit standardmäßig an Subagenten delegiert wird – Normalfall, nicht Ausnahme. Diese Festlegung habe ich getroffen, und einmal sogar nachkorrigiert, weil eine frühere Fassung sie andersherum beschrieben hatte. Die Kette hat drei Glieder, nicht zwei: Ich beauftrage eine Sitzung, die Sitzung beauftragt ihre Agenten, und die Lücke entsteht im zweiten Schritt. Dass ich den nicht selbst ausgeführt habe, ändert nichts daran, wem der Apparat gehört. Genau deshalb heißt es „befohlen, ohne es zu merken“ und nicht „befohlen“.

Die zwei lehrreichsten Scans, einzeln

Eine Datei, die es nicht gibt

Der drittlängste Scan (die vier längsten liegen alle innerhalb von vierzig Minuten beieinander) gehört zu einem Auftrag, der eine Behauptung über einen Typ namens ConfigAccessLevel mitgab und keinen Ort dazu. Der Agent tat das Naheliegende: find / -iname "ConfigAccessLevel.cs". Dieser Prozess lief zwölf Stunden dreiundzwanzig Minuten.

ConfigAccessLevel.cs existiert nicht. Nirgends. Der Typ ist ein enum innerhalb einer anderen Datei – er steht in ConfigScope.cs, dort in den Zeilen 4 bis 17. Nachsehen lassen habe ich das in zwei der drei Fassungen, die von dieser Bibliothek auf meiner Maschine liegen: im Dekompilat-Checkout von vorhin und in der Kopie aus der Bauumgebung des Projekts, jeweils mit ConfigScope.cs als Gegenprobe.

Der Agent konnte das nicht wissen. Er hatte einen Typnamen und den Auftrag, eine Behauptung darüber am Quelltext zu prüfen. Also suchte er die Datei, die so heißt, wie der Typ heißt. Eine völlig vernünftige Annahme, die in diesem einen Fall nicht zutrifft.

Damit konnte dieser Scan nicht enden. Eine Suche nach einer Datei, die es gibt, hört auf, wenn sie sie findet. Eine Suche nach einer Datei, die es nicht gibt, hat keinen solchen Punkt: Sie muss das gesamte Dateisystem durchlaufen, alle Netzfreigaben eingeschlossen, und endet leer. Zwölf Stunden dreiundzwanzig Minuten sind keine ungewöhnlich lange Suche. Sie sind, was dabei herauskommt, wenn man alles ansehen muss.

Die eigentliche Pointe liegt aber eine Ebene tiefer. Hätte der Auftrag den Suchraum genannt, wäre in Sekunden sichtbar gewesen, dass die Datei dort nicht liegt, und warum sie nirgends liegen kann. Der fehlende Ort hat nicht nur Zeit gekostet. Er hat die Einsicht verhindert, die die Suche erübrigt hätte.

Eine Verzeichnisebene höher

Der längste Scan von allen (zwölf Stunden neunundvierzig Minuten) stammt ausgerechnet aus einem Auftrag, der einen exakten, ausgeschriebenen Dateipfad nannte. Von der Sorte, die man sich wünscht. Der Agent hat ihn auch benutzt, zweimal und beide Male erfolgreich: Er las die Methode, um die es ging (Bind), direkt aus ConfigFile.cs; zwei Minuten später las er über denselben Pfad ConfigScope.cs.

Dazwischen aber, eine Sekunde nach dem ersten erfolgreichen Zugriff, suchte er eine dritte Fassung derselben Bibliothek. Denn CoreLib liegt auf dieser Maschine mehrfach: als Original im Dekompilat-Checkout, aus dem er gerade gelesen hatte, als installierte Fassung und als Kopie im Paket-Cache des Mod-SDK, jener Projektumgebung, mit der die Mods gebaut werden. Diese dritte kommt in keinem Auftrag vor, und sie ist die Fassung, die beim Bauen tatsächlich verwendet wird. Der Quelltext, den er gerade gelesen hatte, muss damit nicht identisch sein.

Das Verzeichnis, das es nicht gibt

An erster Stelle stand dabei eine begrenzte Suche, auf das Verzeichnis, in dem das SDK seiner Ansicht nach liegen musste. Dieses Verzeichnis gibt es nicht, jedenfalls heute nicht, als ich es nachprüfen ließ; das SDK liegt eine Ebene höher. Erfahren hat er das nie, und er hat auch nicht darauf gewartet: Die begrenzte Suche und die Suche ab / standen in einer einzigen Kommandozeile, getrennt durch ein Semikolon. Der Vollscan war damit keine Reaktion auf ein leeres Ergebnis, sondern von vornherein mit abgeschickt, der Form nach eine Absicherung für den Fall, dass die begrenzte Suche nichts findet. Ein Semikolon fragt aber nicht, wie der Teil davor ausgegangen ist. Es lief so oder so.

Ob an der geratenen Stelle am Vorabend etwas anderes stand, etwa ein Symlink, ist nicht mehr zu entscheiden: Der Fehler war unterdrückt, und auch die Ausgabe des begrenzten Versuchs ging in derselben 120-Sekunden-Grenze unter, die schon jeden der sieben Scans in den Hintergrund geschoben hat. Die Grenze galt dabei nicht dem Versuch (der war in Sekundenbruchteilen durch), sondern dem Bash-Aufruf, in dem er steckte, samt allem, was darin bis dahin ausgegeben worden war.

Dabei liegt die gesuchte Datei (dieselbe ConfigFile.cs, nur eben in der dritten Fassung) im echten SDK tatsächlich: in dem, das eine Ebene höher beginnt als das Verzeichnis, auf das er gezielt hat, und darin etliche Ebenen tiefer, im Paket-Cache.

Nur einer der beiden Fehler ist teuer

Zwei Fehler liegen hier übereinander. Die falsche Verzeichnisebene kostete die Antwort: eine Ebene höher hätte dieselbe begrenzte Suche die Datei in Sekundenbruchteilen geliefert. Die zwölf Stunden neunundvierzig Minuten über unbeteiligte Rechner kostete sie nicht. Die hingen an dem Semikolon, das die unbegrenzte Suche unabhängig vom Ausgang der begrenzten anhängte.

Man kann das als übertriebene Gründlichkeit lesen: ein Agent, der alles hatte und trotzdem suchte. Diese Lesart ist falsch. Der Auftrag nannte eine von drei Fassungen, der Agent brauchte eine andere. Wo sie liegt, konnte er nur raten.

Nicht kein Ort, sondern nicht der gebrauchte

Beide Fälle haben dieselbe Mechanik, und sie ist nicht die, die ich nach dem Nachzählen vermutet hatte – dort sah es so aus, als fehle schlicht der Ort.

Ein Auftrag, der eine Verifikation verlangt und keinen Ort nennt, ist kein Auftrag zum Prüfen: er ist einer zum Suchen. Aber ein Auftrag, der einen Ort nennt, obwohl es drei gibt, ist es ebenfalls: Er schickt die Suche nach der Fassung, die er verschwiegen hat. Die Unvollständigkeit liegt in beiden Fällen beim Auftraggeber. Entscheidend ist nicht, ob ein Ort im Auftrag stand, sondern ob der gebrauchte darin stand.

Was der Auftraggeber dem Agenten schuldet, ist dabei eine Grenze für die Suche, nicht der Pfad zu einer bestimmten Datei; entscheidend ist allein, ob die Suche dadurch endlich wird. Ein Auftrag kann keinen Ort nennen, er kann ein Verzeichnis nennen, er kann ein Verzeichnis plus Filter nennen, und er kann den exakten Pfad ausschreiben. Zwischen den letzten dreien geht es um Effizienz. Zwischen „kein Ort“ und „irgendein Ort“ geht es darum, ob der Prozess das Gerät verlässt.

Vier Stufen, und wie oft jede vorkam

Das lässt sich messen. Über die achtundzwanzig Aufträge verteilen sich die vier Möglichkeiten so:

  • kein Ort: 24 Aufträge
  • nur ein Verzeichnis: keiner
  • Verzeichnis plus Filter: zwei
  • exakter Pfad: zwei

Wer einen Ort angibt, gibt ihn gleich präzise an. Zwischen den vier und den 24 liegt kein Graubereich.

Die Staffelung ist als Regel also richtig, beschreibt aber kein beobachtetes Verhalten: In der Praxis fällt die Entscheidung binär, und der Fehler ist nicht Ungenauigkeit, sondern Weglassen. Genau deshalb lautet die Regel, die ich daraus mitgenommen habe, nicht „nenne den exakten Pfad“: das ist oft gar nicht erfüllbar, man weiß selbst nicht immer, welche Datei es ist. „Nenne, was du weißt“ ist dagegen immer erfüllbar, und eine unerfüllbare Regel wird ignoriert. Die strengere Fassung wäre in der Praxis die schwächere.

Was hier passiert ist, ist übrigens ausdrücklich kein Agenten-Fehlermodus. Über die gibt es genug zu sagen, und ich habe das an anderer Stelle getan: Fehlermodi in Multi-Agenten-Systemen. Der Agent hat nicht halluziniert, nicht abgekürzt, nicht falsch geschlossen. Er hat exakt getan, was im Auftrag stand.

Und damit gilt die Regel weit über Agenten hinaus. Sie gilt für jede Delegation, bei der jemand etwas prüfen soll, das er nicht finden kann, auch zwischen Menschen.

Drei Artefakte, und zwei von drei Orten

Ich habe eine Faustregel dafür, wann aus einem Vorfall eine Automatisierung wird: „Einmal ist Zufall, zweimal ist ein Muster, dreimal ist eine Regel.“

Nach diesem Tag wäre ein Guard naheliegend gewesen: ein Hook, der ein find ohne Pfadgrenze abfängt, bevor es startet. Ich habe ihn bewusst nicht gebaut. Beim ersten Mal ist die erwartete Häufigkeit unbekannt. Eine teure Position, die man baut und dann pflegt, kann sich nicht amortisieren, wenn der Fall nie wiederkommt. Bei einer Wiederholung wäre er entstanden, also beim zweiten Mal, nicht erst beim dritten. Die Faustregel liefert das Bild, nicht die Schwelle; wann gebaut wird, entscheidet der Schaden. Bei etwas, das einen Nachmittag kostet, kann man den dritten Fall abwarten. Zwölf Stunden Last auf unbeteiligten Rechnern gehören nicht in diese Kategorie.

Drei Artefakte statt eines Guards

Entstanden sind drei andere Dinge. Eine globale Verhaltensregel (die Regel-Notiz von vorhin, nach dem Nachzählen korrigiert): Pfad mitgeben, Vollscan verbieten, Grenze setzen. Eine Meta-Regel über den Zeitpunkt des Automatisierens (die Faustregel von oben, an diesem Tag als Notiz festgehalten), samt der Entscheidung gegen den Guard, die aus ihr folgte. Und, eine knappe Viertelstunde nach dem Aufräumen, eine projektlokale Pfad-Notiz, die festhält, wo der Quelltext von CoreLib auf dieser Maschine liegt.

Die dritte ist die kleinste und die ursachennächste, und das ist kein Zufall. Ein Guard hätte den Agenten gestoppt und ihn ohne die Information dastehen lassen, die er suchte; die Notiz gibt sie ihm. Repariert wird damit an der Stelle, an der der Mangel entsteht (fehlendes Wissen im Projekt), nicht dort, wo er sich äußert: ein Kommando in der Shell.

Sie ist außerdem die einzige der drei, deren Art überhaupt zum längsten Scan passt, zu dem Agenten, der eine dritte, in keinem Auftrag genannte Fassung suchte. Sie nennt nämlich nicht einen Ort, sondern zwei, und sie regelt ihr Verhältnis: welche Fassung maßgeblich ist und wann gegen die andere zu diffen ist. Damit zielt sie genau auf den Fall, den ein schlichtes „gib den Pfad mit“ nicht erwischt, weil ja ein Pfad gegeben war. Und sie richtet sich ausdrücklich an den, der delegiert: Wer einen Subagenten losschickt, gibt einen dieser Pfade mit.

Dass niemand den eigenen Stapel an Regeln und Guards vollständig kennt, ich eingeschlossen, ist ein eigenes Thema, nachzulesen unter Regeln, Guards und der Stapel, den keiner überblickt.

Der Schluss, der mir nicht gefällt

Der Art nach war die Pfad-Notiz die richtige Gegenmaßnahme. In ihrer damaligen Fassung hat sie es trotzdem nicht geleistet: Sie nannte zwei Orte, belegt sind drei, und der fehlende ist die Kopie im Paket-Cache des Mod-SDK, genau die Fassung, nach der der längste Scan gesucht hat.

Auch wenn sie noch am selben Tag ergänzt wurde (gut zwei Stunden, nachdem sie entstanden war), war die Gegenmaßnahme an derselben Stelle unvollständig wie die Aufträge, die sie verhindern sollte: Sie nannte nicht den gebrauchten Ort.


This article is also available in English on Medium.

Fokus-Webinar: Der #1-Killer für Dein KI-Projekt
Fokus-Webinar: Advanced Unstructured Data
Fokus-Webinar: Daten, aber schnell!
Fokus-Webinar: Vertrauenswürdige KI mit Observable RAG
Fokus-Webinar: LLMs als Wissensgedächtnis
Fokus-Webinar: Personenbezogene Daten & KI – ie Pseudonymisierung hilft
Fokus-Webinar: Der #1-Killer für Dein KI-Projekt
Fokus-Webinar: Advanced Unstructured Data
Fokus-Webinar: Daten, aber schnell!
Fokus-Webinar: Vertrauenswürdige KI mit Observable RAG
Fokus-Webinar: LLMs als Wissensgedächtnis
Fokus-Webinar: Personenbezogene Daten & KI – ie Pseudonymisierung hilft

Dein Kontakt zu uns

Avatar von Sven Pöche

Dein Thema?

Das Thema interessiert dich? Wenn Du fragen hast, dann melde dich ganz unverbindlich bei uns!

Wie dürfen wir Dich ansprechen?

Höflichkeit ist uns wichtig!

Wie können wir Dich erreichen?

Womit können wir behilflich sein?


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.