HT001 · Ein Terminal-IDE von Grund auf schreiben¶
2026-09-07: Ein Agent hat unter flower von Grund auf ein C/C++-Einzeldatei-IDE geschrieben, das im macOS-Terminal läuft. Er lief 10.4 Stunden am Stück, verbrauchte $171.62 und lieferte 12,212 Zeilen Produktcode und 13,291 Zeilen Tests ab. Zwischendurch fiel einmal das Netz aus, er lief von selbst weiter bis zum Ende. Diese Seite ist keine Leistungsschau — sie misst jede These des Frameworks einmal in realem Maßstab durch, einschließlich der beiden Thesen, die dabei nicht standhalten: Dass der Coordinator „Berechtigungsgrenzen häufig ignoriert“, ist falsch; und in seiner eigenen Abnahme hat er einen Punkt falsch entschieden.
Die vollständige Aufzeichnung (inkl. 19MB Transcript) liegt in ChenyuHeee/cppide. Jede Zahl unten wird von tools/analyze_run.py aus sessions.db berechnet und ist reproduzierbar:
Woher die Zahlen auf dieser Seite stammen
Das runs/ von HT001 liegt im oben genannten cppide-Repo, nicht im flower-Repo. Die Zahlen auf dieser Seite sind also unverändert aus der damaligen Laufaufzeichnung übernommen; beim Schreiben dieser Seite wurde nicht noch einmal gegen sessions.db nachgerechnet — wer nachrechnen will, muss zuerst cppide klonen und dann den obigen Befehl ausführen. HT002 ist umgekehrt: Dort liegt die Rohaufzeichnung in diesem Repo, die Zahlen jener Seite wurden Punkt für Punkt nachgerechnet, und die zwei Stellen, die nicht aufgingen, stehen direkt auf der Seite.
Die Form dieses Runs¶
| Posten | Wert |
|---|---|
| Aufgabe | „Bau ein C/C++-Einzeldatei-IDE für das macOS-Terminal, für ICPC-Training“ |
| Kosten | $171.62 — Clarify $0.3704 / 5 Turns, Arbeit $171.2476 / 31 Turns |
| Dauer | 0.06h + 10.44h |
| Kontext im Main Thread | 28.7K → 185.9K, über 70 Modell-Turns monoton wachsend, durchgehend keine Compaction |
| Arbeitsteilung | Coordinator 32 Tool-Aufrufe vs. Subagent 1,893; 94.8% der Textzeichen liegen im Subagent |
| Cache | Gesamt-Input 299.4M Token, Trefferquote 96.1% |
| Ergebnis | src/ 12,212 Zeilen, tests/ 13,291 Zeilen (rund 3,000 Assertions), README 44K |
| Version | flower vor dem Goal Guard, dieser Run hatte also kein Verdict pro Turn |
Auf dieser Seite gibt es zwei verschiedene „Turns“
Die 5 Turns / 31 Turns in der Tabelle sind die num_turns aus runs/manifest.json, eine Zahl pro Schritt; der „Turn 70“ in der Kontextkurve unten zählt die Modell-Turns im Main Thread, also jede Assistant-Nachricht. Beide zählen nicht dasselbe, sie lassen sich nicht ineinander umrechnen.
Eins — Kontextökonomie: 94.8% des Geschehens erreichen den Main Thread nie¶
Das ist die Kernthese des Frameworks — im Coordinator liegen nur Entscheidungen, das Geschehen sinkt in Subagents und auf die Platte. Zum Mechanismus selbst siehe Kontextökonomie.
| Main Thread | Subagent | Anteil Subagent | |
|---|---|---|---|
| Modell-Turns | 70 | 3.0K | 97.7% |
| Textzeichen | 200.1K | 3.6M | 94.8% |
| Tool-Aufrufe | 32 | 1,893 | —— |
Dieser Run hat 23 Subagents losgeschickt, Output-Token maximal 184,377, Median 72,197, minimal 11,684.
Von 1,893 handfesten Tool-Aufrufen (1,034 Bash / 471 Read / 258 Edit / 126 Write) sind nur 32 im Blickfeld des Coordinators gelandet — 59:1. Pro entsandtem Subagent bleiben im Schnitt 82 Tool-Aufrufe für ihn unsichtbar.
Frühere Kleintests maßen 83%, in realem Maßstab sind es 94.8%. Je größer der Maßstab, desto größer der Gewinn der Arbeitsteilung — denn der Aufwand für Task Brief und Rückmeldung ist pro Entsendung fix, während das ausgesperrte Geschehen mit der Komplexität der Aufgabe wächst.
Zwei — Die Kontextkurve: zum ersten Mal wissen wir, wo die Obergrenze von „long-horizon“ liegt¶
input + cache_read + cache_creation jeder Assistant-Nachricht ist genau der Kontext, den das Modell in diesem Turn gesehen hat:
Turn 1 28.7K ← Start-Untergrenze (früher rund 34k gemessen, passt)
Turn 20 35.2K
Turn 35 108.6K
Turn 50 158.2K
Turn 70 185.9K ← Ende
- 70 Turns monoton wachsend, Steigung rund 2.2K/Turn
- Durchgehend keine Compaction (
DISABLE_AUTO_COMPACT=1; der einzige Rückgang ist der leere Eintrag im Turn des Netzausfalls) - 18.6% des 1M-Fensters verbraucht
Extrapoliert man diese Steigung, ist die Wand bei rund 440 Turns. Diese 70 Turns haben 12K Zeilen Code produziert, die reale Obergrenze für long-horizon in der aktuellen Form liegt also etwa beim 6-fachen dieses Runs. Das ist eine Zahl, mit der man Entscheidungen treffen kann; vorher konnte man nur raten.
Um sie zu durchbrechen, muss das Wachstum des Main Thread sublinear werden — mehr auf die Platte sinken, weniger im Dialog bleiben.
Drei — Cache-Trefferquote 96.1% — nur deshalb funktionieren die $171.62¶
Gesamt-Input 299.4M Token
Cache-Treffer 287.5M (96.1%)
Cache-Neuanlage 11.8M
ungecacht 6.2K
Output 2.3M (davon thinking 653.9K)
299M Input-Token haben nur $171 gekostet, weil 96% zum Cache-Preis liefen.
Folgerung: Die Wirtschaftlichkeit eines long-horizon Runs steht und fällt mit der Cache-Trefferquote, und die hängt an der Stabilität des Kontext-Präfix. Jede Optimierung, die „den Kontext umsortiert“ — alte Inhalte wegschieben, vorne einen neuen System-Prompt einfügen — zerschlägt den Cache, und die gesparten Token wiegen den verlorenen Cache-Rabatt womöglich bei Weitem nicht auf. Das war vorher nie gemessen, jetzt gibt es Zahlen.
Das ist auch ein Nebengrund, warum flower Handoff statt Compaction verwendet: Ein Handoff öffnet eine neue Session und baut den Cache von vorne auf, das Präfix bleibt innerhalb einer Generation durchgehend stabil.
Vier — Workbench-Wiederverwendung: 61 Skripte, keines nach dem Schreiben weggeworfen¶
Die These der Workbench lautet: „Skript einmal schreiben, danach direkt ausführen, nie wieder neu schreiben.“ Verifiziert wird das nicht, indem man den Agent fragt, sondern indem man zählt, wie oft die Skripte in .flower/scripts/ im Transcript per Write/Edit geschrieben und per Bash ausgeführt wurden:
61 Skripte: 95-mal geschrieben, 331-mal ausgeführt
mehr als 1-mal ausgeführt (echt wiederverwendet): 56, also 92%
geschrieben, aber nie ausgeführt: 0
Verhältnis Ausführungen/Schreibvorgänge: 3.48
wave5-verify.sh wurde 5-mal geschrieben und 45-mal ausgeführt — iterativ geschliffen und dann wieder und wieder benutzt, genau die gewünschte Form. Kein einziges Skript wurde nach dem Schreiben weggeworfen.
Fünf — Der Coordinator wurde 1-mal abgelehnt, und schuld war die Whitelist¶
Der Verdacht vor der Messung war: „Der Coordinator ignoriert Berechtigungsgrenzen häufig und löst massenhaft abgelehnte Aufrufe aus.“ Die Messung hat diese Prämisse zerlegt.
Von 32 Tool-Aufrufen wurde 1 abgelehnt (3.1%), und zwar dieser:
which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs
Segmentweise Prüfung: echo durchgelassen, ls durchgelassen, which geblockt — es stand nicht in der Tabelle der Ephemeral Commands.
Der gesamte Befehl ist rein lesend, eine standardmäßige Umgebungssondierung. Das Modell hat vollkommen richtig gehandelt, die Whitelist war unvollständig und zwang es, für ein einziges which g++ die Startkosten eines Subagents zu zahlen (rund 4.3k Token).
Behoben: which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers nachgetragen. Bei command und type wird nur die Abfrageform durchgelassen — ein nacktes command rm -rf / ist Ausführung, das fehlende -v wäre eine offene Hintertür, in tests/glance.py bewachen das 7 Adversarial-Samples.
Das Merkenswerteste daran ist nicht der Patch, sondern die Methode. Eine Architekturdiskussion, die sich lange hinzog, hatte als Ausgangsprämisse in echten Daten 1/32, und die Zuschreibung war verkehrt herum — das Problem lag nicht am Modell, sondern an der Whitelist. Erst messen, dann reden.
Sechs — Netzausfall und Weiterlauf: zum ersten Mal von einem echten Fehler validiert¶
01:52:40 tool_result: "Agent terminated early due to an API error" ← ein Subagent wurde unterbrochen
01:55:41 assistant: "API Error: ENOTFOUND" ← Main Thread empfängt es
↓ Probe hängt und wartet auf die Wiederherstellung des Netzes
resumed=True, dieselbe Session lief weiter, noch gut 8 Stunden bis zum Abschluss
Im Manifest steht attempts=2 / resumed=True / ok=True. 10 Stunden Arbeit mussten nicht von vorn beginnen. Bis dahin hing Resilienz unter „noch nicht verifiziert“ — das Netz zu kappen erfordert sudo und einen hosts-Eingriff, und das will niemand während eines echten Runs tun.
Diese synthetische „API Error“-Nachricht wurde vom PruningSessionStore beim Laden entfernt, das Modell hat sie nie gesehen — damit ist auch das Design des Pruning an einem echten Fehler validiert.
Zwei Probleme, die dabei sichtbar wurden (siehe issue #2):
- Die Wiederherstellung passiert auf der Framework-Ebene, nicht auf der Transportebene. Der Preis ist ein vollständiges Replay des Kontexts, und der Kontext war damals groß.
- Die halbfertige Arbeit des unterbrochenen Subagents ging verloren. Resume rettet den Main Thread, Unterbrechungswiederherstellung auf Subagent-Ebene ist ein anderes Problem.
Sieben — Ist seine Selbstverifikation echt?¶
Hier ist das Misstrauen am angebrachtesten: Ein Agent schreibt seine Tests selbst, führt sie selbst aus und erklärt selbst, sie seien bestanden — das wird leicht zur Formalie.
Im Nachhinein wurde ein unabhängiger Audit-Subagent losgeschickt, der statisch alle 58 Skripte und 22 Artefakte gelesen hat. Das Fazit: echt, und von seltener Strenge:
- 0 tautologische Assertions. Im ganzen Repo kein Muster der Art
[ 1 = 1 ]. || truekommt 8-mal vor, 7-mal berechtigt (grep -cliefert ohne Treffer 1,ulimit-Absicherung und Ähnliches).- Geprüft wird Verhalten, nicht die Existenz von Dateien.
audit-idle-cost.sh:53lässt das IDE 60 Sekunden in einem pty leerlaufen und zählt die beim lokalen Fake-Server tatsächlich eingegangenen Requests; PASS nur bei-le 2.fix-p1-idle-verify.sh:100-110geht weiter: Es prüft, dass im Leerlauf genau 1-mal angefragt wird, und dass der Ghost weiterhin angezeigt wird — damit die Drosselung nicht die Funktion mit erschlägt. - Echte Kindprozesse.
wave5-pty-drive.cpp:316nutztforkptyund lässt die TUI wirklich laufen;audit-fake-ai-server.pyist ein echter Socket-Server mit fünf Fehlerinjektionen/err500 /err401 /badjson /drop /slow; auf allen AI-Pfaden wirdCPPIDE_API_KEYzwangsweise geleert.
Zwei Dinge lagen über der Erwartung.
Er hat für seinen eigenen Verifier eine Positiv- und Negativkontrolle gebaut. 验收审计.md:29-34: erst ZZZ_NEVER_APPEARS_ZZZ einspeisen, um zu bestätigen, dass der Driver FAIL meldet, dann den echten String, um PASS zu bestätigen — damit nicht „mit einer Maschine auditiert wird, die immer PASS sagt“. Das ist die Verifikation des Verifiers selbst.
Er hat seine eigenen schwachen Assertions von sich aus offengelegt. Das for m in $must in wave5-b4-ai-states.sh:114 zerlegt "AI 错误 请求超时" an Whitespace in drei Teilstrings und grept jeden einzeln, und AI sowie 错误 kommen auf jedem Screen zwangsläufig vor — nominell „3 bestanden“, tatsächlich hat nur eine Trennschärfe. Diesen Punkt hat er selbst in 验收审计.md:576-582 geschrieben, nicht der Auditor hat ihn ausgegraben.
Gleiches gilt für: wave5-verify.sh:296-302 erklärt in 7 Kommentarzeilen, warum B3b auf SKIP herabgestuft wurde (Timing-Assertion auf dem letzten Screen, ohne Bezug zum Produktcode), und vermerkt, „Prüflogik und Toleranzbereich wurden um kein Zeichen verändert“; :236-238 hält fest, dass er eine eigene, früher geschriebene Fake-Assertion gefunden und behoben hat — if pgrep ... fällt in einem schlanken Image ohne pgrep still in den else-Zweig und wird zum Dauer-PASS.
Die qualitativen Belege für Wiederverwendung decken sich mit den Zahlen aus Abschnitt vier: audit-fake-ai-server.py wird von 7 Skripten wiederverwendet; wave5-pty-drive.cpp wird von 5+ Kompilaten wiederverwendet und cacht die Kompilierung per [ -x "$DRIVE" ] ||; der Driver von wave4 → wave5 ist eine Erweiterung, kein Neuschrieb (der Diff ändert nur 1 Zeile und fügt 343 Zeilen hinzu, die 281 Zeilen aus wave4 bleiben unangetastet).
All dieses Verhalten geschah auf einer Version ohne Goal Guard. Die einzige Einschränkung waren die 10 entscheidbaren Abnahmekriterien im Brief. Das heißt: „Fertig“ in entscheidbarer Form aufzuschreiben, löst für sich genommen schon Selbstprüfung und Selbstoffenlegung aus. Der Grenznutzen des Goal Guard ist gegen diese Baseline zu messen, nicht gegen null.
Aber der nächste Abschnitt zeigt: Die Selbstprüfung ist technisch stark, hat aber beim wichtigsten Punkt das Endurteil falsch gefällt.
Der „Auditor“ hier ist nicht der Judge
Der unabhängige Auditor in Abschnitt sieben und acht ist ein vom Coordinator aus eigenem Antrieb entsandter Subagent, nicht der Judge des Goal Guard — dieser Run hatte überhaupt keinen Goal Guard. Beides liest sich ähnlich, tut aber Unterschiedliches: Der Auditor ist eine nachträgliche Aufarbeitung, der Judge ist das Verdict-Gate am Ende jedes Turns.
Acht — Stimmt seine Selbstbewertung? — Abnahme 1 wurde falsch entschieden¶
Ein zweiter unabhängiger Auditor wurde losgeschickt, um die 10 Abnahmekriterien Punkt für Punkt gegen den Code zu prüfen.
Abnahme 2–10 treffen alle zu, die Codebelege liegen vor. Dass der Übungsmodus keinen Code einfügt, ist sogar strukturell garantiert: einziger Sink-Entscheidungspunkt ai.h:117 + einziger Landing-Switch app.cpp:590-615 + vier Early-Returns editor.cpp:679-683, keine verstreuten ifs. Auch die in der Selbstprüfung gefundenen P1–P4 treffen alle zu, alle Fixes sind umgesetzt (P4 halb zutreffend: die blaue Pair-Zeile ist toter Code, ghostAttr() ging ohnehin über A_DIM).
Aber Abnahme 1 wurde falsch entschieden.
Kriterium im Original: "Kompiliert eine eigenständige ausführbare Datei, die direkt im macOS-Terminal läuft"
Geliefertes Artefakt: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
Sein Verdict: bestanden
Dieser Punkt wurde von Hand mit file nachgeprüft.
Er hat es nicht verschwiegen — die Überschrift von §4 in 交付说明.md lautet wörtlich „Größtes unverifiziertes Risiko: nie ein einziges Mal auf macOS gelaufen“, und §6 des Audits listet sechs unverifizierte macOS-Flächen. Er hat das Risiko offengelegt, diese Tatsache aber nie auf das Verdict zu Abnahme 1 zurückgeführt, obwohl der Wortlaut von Abnahme 1 genau diese Sache verlangt.
Aber das ist kein Beleg dafür, dass „ein unabhängiger Judge nötig ist“¶
Man liest das leicht als „also braucht es den Goal Guard“. Falsch, und diese Lesart ist genau erst schießen, dann die Zielscheibe malen — also exakt die motivierte Argumentation, die der vorige Abschnitt gerade kritisiert hat.
Der damalige Goal Guard hätte diesen Punkt in der Standardkonfiguration mit hoher Wahrscheinlichkeit ebenfalls übersehen. judge() hat per Default can_run=False, als Tools nur Read / Glob / Grep — er kann file gar nicht ausführen. Er hätte den Quellcode gelesen, in Makefile:25-38 tatsächlich einen Darwin-Zweig gesehen und auf „erreicht“ entschieden. Exakt dasselbe Versagen wie die Selbstprüfung.
Die eigentliche strukturelle Tatsache¶
Abnahme 1 ist in der Umgebung dieses Runs prinzipiell nicht verifizierbar.
Sie behauptet etwas über „das Verhalten des Artefakts auf macOS“, während der gesamte Run in einem Linux-Container lief. Kein Judge kann in einem Container ein macOS-Binary verifizieren, egal ob er selbstprüfend oder unabhängig ist. Das Problem ist nicht, „wer entscheidet“, sondern dass dieser Punkt wie ein gewöhnliches entscheidbares Kriterium behandelt und dann entschieden wurde.
Drei weitere Funde des Auditors (in der Selbstprüfung nirgends erwähnt)¶
- Die Kontingent-Semantik von
doTab():app.cpp:909ruft nachacceptGhost()bedingungslosnoteActivity()→gen_++→ Kontingent wird wieder freigegeben, also löst jedes Tab-Akzeptieren 500ms später erneut eine automatische Anfrage aus. Die Analyse zu P1 (Leerlauf verbrennt Geld) hat nur die drei AbbruchpunktesetMode/cancelInFlight/askNowgeprüft und diesen häufigsten Geldpfad übersehen. - Das „Grau“ aus Abnahme 6 gilt wörtlich nur im 256-Farben-Terminal; in der 8-Farben-Stufe ist es fest auf
A_DIMverdrahtet (dunkler, nicht grau). - In
验收审计.mdsteht in der Zwischenüberschrift von §7 „9 Abnahmekriterien“, die Tabelle hat aber 10 Zeilen — ein Tippfehler, aber ausgerechnet in der Kopfzeile der Gesamtbewertung.
Neun — Spill und finale Workbench-Größe¶
103 Spills, 791.4K Zeichen wurden zu Pfadzeigern, ohne dauerhaften Platz im Kontext.
.flower/ am Ende: 58 Skripte (250K), 22 Artefakte (330K), 5 Notizen (43K).
Verbleibende Risiken¶
Lieferziel ist macOS, aber die gesamte Verifikation fand in einem Debian-aarch64-Container statt, null Verifikation auf echter macOS-Hardware. Das ist die reale Nebenwirkung der Container-Isolation: Aus Sicherheitsgründen wird er in Linux eingesperrt, ausliefern soll er aber für macOS. Das README des Projekts hält das in §10.24 ehrlich fest und listet 4 Risikopunkte, schließt den Kreis aber nicht.
Diese Lücke wurde später von HT002 geschlossen — dieser Run hat genau dieses Binary tatsächlich auf macOS installiert.
Ein unerwarteter Nebeneffekt: Im Container ist nur /work sichtbar, deshalb gibt es im 19MB-Transcript null Leaks des Host-Benutzernamens /Users/hechenyu (/work kommt 11,495-mal vor). Die Isolation hat nebenbei auch die Pfade isoliert.
Was dieser Run nicht verifizieren konnte¶
- Goal Guard: Dieser Run liegt davor. Der Coordinator hat von sich aus ein unabhängiges Audit organisiert, technisch sehr solide, aber beim wichtigsten Punkt falsch entschieden (Abschnitt acht). Beide Schlüsse gehören zusammen notiert — ein Brief mit klaren, entscheidbaren Kriterien löst schon Selbstprüfung aus, die Baseline des Goal Guard ist also nicht null; und der Goal Guard in der damaligen Standardkonfiguration hätte diesen Punkt ebenfalls übersehen, er ist also auch kein Allheilmittel.
- Verhalten im 1M-Fenster: nur 18.6% genutzt, die Grenze wurde nie berührt.
- Verhalten der Mikro-Compaction unter
DISABLE_AUTO_COMPACT=1: Es wurde durchgehend keine Compaction ausgelöst, bleibt also Schlussfolgerung.
Was dieser Run am Framework geändert hat¶
| Befund | Umgesetzte Änderung |
|---|---|
which wurde geblockt (Abschnitt fünf) | is_ephemeral() um which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers ergänzt; tests/glance.py um 7 Adversarial-Samples erweitert, die die Abfrageform von command / type absichern |
| Abnahme 1 falsch entschieden (Abschnitt acht) | Regel 1 der JUDGE_RULES geändert auf geprüft wird das Artefakt, nicht der Quellcode — was laufen kann, wird gestartet; was nicht laufen kann, wird als Artefakt selbst geprüft (Plattform, Größe, Ladbarkeit) |
| „Ich habe das Risiko doch offengelegt“ führte zu einem Bestanden | Das Verdict muss „nicht erreicht“ und „hier nicht verifizierbar“ unterscheiden. Verdict.parse erkennt jetzt 无法验证 / 没法验证 / 无法判定 als dritten Zustand unreachable → taucht auf und fragt den Menschen, ein Bestanden ist strikt verboten |
| Abnahme 1 ist in dieser Umgebung prinzipiell nicht verifizierbar | Der Schritt der Zielsetzung verlangt jetzt, in dieser Umgebung nicht verifizierbare Punkte sofort zu markieren, statt sie erst nach getaner Arbeit zu entdecken |
| Netzwiederherstellung auf Framework-Ebene, halbfertige Subagent-Arbeit verloren (Abschnitt sechs) | Als issue #2 vermerkt, nicht geschlossen |
Die ersten drei Punkte gingen in HT002 erstmals gegen eine echte API — dort hat „das Artefakt prüfen, nicht den Quellcode“ auf der Stelle einmal gerettet — aber dieselbe Regel hat auch ein neues Versagen erzeugt.