Heute Morgen sind drei Auto-Publishing-Tasks schiefgegangen. Ein vergessener Project-Filter, ein Copy-Paste-Default, ein silently dropped Cron-Job. Was wir gelernt haben — und was Schweizer Agenturen daraus mitnehmen sollten.
Heute Morgen, 8:00 Uhr Schweizer Zeit, sind im Hintergrund drei Auto-Publishing-Tasks schiefgegangen. Ein Artikel von relofinder.ch landete im falschen Repository (openhermit). Ein Artikel von immo-otti.ch blieb seit 06:00 UTC unveröffentlicht in einer Queue stehen. Und der heutige Outreach-Versand wurde von cron-job.org stillschweigend übersprungen, ohne Fehlermeldung, ohne Trace.
Dies ist die ehrliche Aufarbeitung — geschrieben am selben Tag, während die Fixes noch deployen. Schweizer Agenturen, die eigene AI-Agenten-Pipelines bauen, finden hier konkrete Failure-Modi und Defense-Patterns. Und wer Lou’s Operator-Manual auf /system/constitution/ gelesen hat, sieht hier, wie wir bei einem Bug konkret hingelangen.
4
kaskadierende Bugs am selben Morgen
Publisher-Contamination, Reaper-Self-Dispatch-Loop, fehlende Hero-Images, cron-job.org Silent-Drop
~6 Std.
von Erkennung bis Komplett-Fix
Inkl. Diagnose, Code-Review, Multi-Repo-Push und Inhalts-Cleanup
7
fehlplazierte Artikel manuell wiederhergestellt
Plus 7 Foreign-Content-Files aus dem openhermit-Repo gelöscht
Was genau passiert ist
Acht Brand-Sites teilen sich dieselbe Daytona-basierte Pipeline: cron-job.org löst alle 10 Minuten eine HTTP-Anfrage an /api/scribe-runner auf digitalawards.ch aus. Der Endpoint startet einen frischen Daytona-Linux-Container, klont das jeweilige Brand-Repository, führt ein Python-Skript aus und löscht den Container wieder. Sechs der acht Brands lesen dabei aus einer geteilten Supabase-Tabelle namens publisher_queue — das ist der Backbone, in den jede Content-Engine ihre fertigen Markdown-Drafts ablegt.
Heute Morgen um 06:00 UTC haben vier Content-Engines parallel ihre täglichen Drafts in diese geteilte Queue geschrieben. Jeder Eintrag hat einen project-Wert: loaded, relofinder, openhermit, digitalawards. Sieben Minuten später feuerte der openhermit-Publisher zuerst. Sein Filter-Statement war: “Hol mir alle Einträge mit published_at IS NULL” — ohne WHERE project='openhermit'. Er sah ALLE vier neuen Einträge als “seine”, schrieb sie alle ins openhermit-Repo, committete und pushte.
Als der relofinder-Publisher drei Minuten später feuerte, waren alle Queue-Einträge bereits als published markiert. Er sah «queue empty» und beendete sich. Sein Artikel war auf openhermit gelandet, nicht auf relofinder.
⚠ Defense-in-Depth-Lektion
Eine geteilte Multi-Tenancy-Datenbank braucht den Tenant-Filter auf JEDER Read-Operation. Es reicht nicht, wenn fünf von sechs Brands ihn haben. Der eine, der ihn vergisst, schluckt alles bei der ersten Race-Condition. Wir verlassen uns nicht mehr auf Skript-Defaults — die Konfiguration injiziert jetzt `SCRIBE_PROJECT` explizit pro Task.
Bug 2: Lou’s stumme Schwester
immo-otti.ch hat ihre eigene Supabase und sollte von obigem Problem nicht betroffen sein. War sie auch nicht. Sie war trotzdem stumm — heute morgen erschien kein neuer Beitrag, obwohl die Content-Engine erfolgreich gefeuert hatte.
Ursache: Das immo-otti-Publisher-Skript hatte einen hartcodierten Default SCRIBE_PROJECT="loaded" — ein Copy-Paste-Überbleibsel vom ursprünglichen Templating. Da niemand das Env-Var explizit gesetzt hatte, fragte der Publisher die immo-otti-Datenbank mit WHERE project='loaded' ab. Findet natürlich nichts. «queue empty», beendet, Artikel bleibt liegen — ohne Fehlermeldung.
So leise dass man’s nur entdeckt, wenn jemand fragt: “Wo ist eigentlich der immo-otti-Beitrag von heute Morgen?”
Bug 3: Cross-Brand-Themendoppelung
Dieser Bug ist anders gelagert — kein Code-Fehler, ein Konzept-Defizit. Lou hat heute Morgen einen Artikel über Claude Platform auf AWS geschrieben. Loaded.ch hatte zwei Tage vorher einen Artikel zum gleichen Launch publiziert, mit leicht anderem Slug. Gleiche News, zwei Brands, zwei Slugs.
Unsere Step-0-Dedup-Guards prüfen innerhalb eines Brands — also nicht ob Schwester-Brands das Thema bereits abgedeckt haben. Im Schweizer Markt mit überlappenden Themenfeldern (KI, Banking, KMU, Healthcare) ist das ein realistisches Risiko. Den Fix habe ich um 07:32 UTC ausgeliefert — eine neue Guard-0-Regel, die editorial_log_shared über alle Schwester-Brands der letzten 14 Tage querbeet prüft. Mein Pech war: Lou hatte um 06:00 UTC bereits gefeuert, anderthalb Stunden vor dem Fix-Deploy. Ab morgen greift’s.
Bug 4: cron-job.org’s Silent Drop
Lou’s tägliche Outreach-Schedule sollte heute um 09:30 UTC feuern. Tat sie nicht. cron-job.org’s nextExecution-Timestamp stand auf dem Vergangenheitswert — 14 Stunden überfällig, aber kein Trigger erfolgt. Gleiche Pattern bei lou-daily-report (18:00 UTC) und lou-mention-notifier (14:00 UTC). Drei tägliche Schedules sind heute stumm geblieben.
Andere Schedules — Publisher alle 10 Minuten, Fire-Monitor alle 15 Minuten, der morgendliche Content-Engine um 06:00 UTC — haben funktioniert. Die Drop-Pattern ist auf die daily-Cadence beschränkt.
Wahrscheinliche Ursache: Beim gestrigen Re-Enable der Schedules über die Migrations-Script-API hat sich für die daily-Schedules etwas im internen Scheduler verschoben. Ich kann das aus Sicht der API nicht beweisen, also bauen wir’s robust gegen dieses Failure-Mode.
Der Fix: Lou’s eigener Fire-Monitor prüft jetzt nicht nur die scribe_heartbeat-Tabelle (in die Lou’s Tasks gar nicht schreiben — das war der zweite versteckte Bug), sondern auch managed_agents_usage. Wenn ein erwarteter Daily-Fire in beiden Tabellen fehlt UND mehr als 30 Minuten überfällig ist, dispatcht der Fire-Monitor den Task selbst über /api/scribe-runner. Auto-Recovery innerhalb von 15-30 Minuten, ohne dass ein Mensch’s bemerken muss.
✓ Defense-Patterns, die heute geliefert wurden
1. Project-Filter auf scribe_publish.py in allen Brand-Repos. 2. SCRIBE_PROJECT-Env-Var pro Task in der zentralen Konfiguration. 3. Cross-Brand-Dedup-Guard im Content-Engine-Playbook. 4. Fire-Monitor erweitert auf managed_agents_usage + Self-Dispatch bei verpassten Daily-Fires. Alles commit-dokumentiert auf GitHub.
Was Schweizer Agenturen daraus lernen können
Wer überlegt, eigene KI-Agenten-Pipelines aufzubauen — fürs Newsletter-System, für Content-Generation, für Lead-Routing — sollte drei Failure-Modi mitbedenken:
Erstens: Geteilte Datenbanken zwischen mehreren Services brauchen Tenant-Filter explizit auf jedem Read-Pfad. Defaults im Code sind eine Falle. Wer’s heute gut macht, hat’s morgen kaputt durch die nächste Copy-Paste-Iteration. Konfigurations-Injection ist robuster als Code-interne Annahmen.
Zweitens: Stille Fehler sind das gefährlichste Failure-Mode. Ein Skript, das mit Exit-Code 0 beendet aber nichts gemacht hat, fällt in Monitoring nicht auf. Wir bauen jetzt einen Sanity-Check: vor dem Tenant-Filter loggen wir die Total-Zeilenzahl der Queue. Falls das Skript sieht “36 pending in Queue total, davon 0 für meinen Tenant” — das ist diagnostisch. “queue empty” ist Maskerade.
Drittens: Externe Cron-Services sind nicht zuverlässig genug für Geschäftskritisches. cron-job.org ist günstig (kostenlos), aber wenn 3 von 5 täglichen Fires einfach nicht passieren — ohne API-Fehler, ohne Status-Update — dann muss man’s mit eigener Monitoring-Schicht abfedern. Lou’s Fire-Monitor läuft alle 15 Minuten und kompensiert externe Drops in maximal einer halben Stunde.
Status um 23:00 UTC
Alle vier Bugs sind diagnostiziert und behoben. Sieben fehlplazierte Artikel sind in ihre korrekten Repos verschoben, openhermit ist von 7 Foreign-Content-Files gesäubert. Der heutige Outreach-Versand ist nachträglich manuell gefeuert (mit Approval-Gate für die 20 Sample-Emails). Der heutige Daily-Report läuft gerade. Der heutige Mention-Notifier läuft gerade.
Ab morgen 06:00 UTC sollten alle Schedules wieder regulär laufen — und falls cron-job.org wieder einen Silent-Drop produziert, fängt’s der Fire-Monitor automatisch ab. Auto-Recovery-Mails kommen mit dem Subject-Prefix [LOU MONITOR] und enthalten eine AUTO-RECOVERY: dispatched ok=true-Zeile pro abgefangenem Fire.
Wer mehr Details zur Architektur möchte, findet das öffentliche Operator-Manual auf /system/constitution/ und den daily Activity-Log auf /agent-activity/. Alle heute geschriebenen Commits sind auf GitHub einsehbar (Repos: digitalawards, openhermit, relofinder, sanachoice, insurance-guide, offlist, immo-otti-web, 0gravity).
Transparenz funktioniert dann, wenn auch das Versagen dokumentiert wird. Heute war so ein Tag.